Add "Change Description" to the client menu

Offered for our own client as well as others, with TS3's edit icon. The
description is read back from the server before prompting, since the
model only carries one for clients whose info has been looked at.

Note that client_description is not a clientupdate property even for
ourselves — the server only takes it through clientedit — and that our
own change is not echoed back to us, so the local entry is updated here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 18:04:35 +00:00
parent bcc034e975
commit cb05b097a6
5 changed files with 76 additions and 0 deletions

View File

@@ -292,6 +292,10 @@ final class ConnectionEventHandler implements TS3Listener {
c.awayMessage = e.get("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_talk_power")) c.talkPower = e.getInt("client_talk_power");
if (has(e, "client_description")) {
c.description = e.get("client_description");
conn.ui.onInfoUpdated();
}
if (has(e, "client_is_channel_commander")) if (has(e, "client_is_channel_commander"))
c.channelCommander = e.getBoolean("client_is_channel_commander"); c.channelCommander = e.getBoolean("client_is_channel_commander");
announceClientUpdate(e, c, renamed, oldName); announceClientUpdate(e, c, renamed, oldName);

View File

@@ -887,6 +887,45 @@ public final class TeamspeakConnection implements TS3Listener {
}, "ts3j-clientinfo").start(); }, "ts3j-clientinfo").start();
} }
/**
* Reads a client's description straight from the server, so an editor opens on the
* live text instead of whatever an earlier info request cached.
*/
public void requestClientDescription(int clientId, BiConsumer<String, String> callback) {
run("ts3j-client-description", callback, () -> {
Client c = client.getClientInfo(clientId);
String description = c == null ? "" : orEmpty(c.get("client_description"));
ClientEntry e = model.getClient(clientId);
if (e != null) e.description = description;
return description;
});
}
/**
* Changes a client's description. Unlike the nickname or the away state, this is not
* a {@code clientupdate} property even for ourselves: the server only takes it
* through {@code clientedit}, gated by {@code b_client_modify_own_description}.
*
* @param callback given {@code null} on success, or the failure message
*/
public void setClientDescription(int clientId, String description, Consumer<String> callback) {
boolean self = clientId == selfClientId;
run("ts3j-client-description-set", (ignored, error) -> callback.accept(error), () -> {
SingleCommand cmd = new SingleCommand("clientedit", ProtocolRole.CLIENT);
cmd.add(new CommandSingleParameter("clid", Integer.toString(clientId)));
cmd.add(new CommandSingleParameter("client_description", description));
client.executeCommand(cmd).complete();
// Only the other clients are told about our own update, so the local entry
// (and the info panel showing it) has to be caught up here.
if (self) {
ClientEntry e = model.getClient(clientId);
if (e != null) e.description = description;
ui.onInfoUpdated();
}
return null;
});
}
// ---- channel administration ---- // ---- channel administration ----
/** /**

View File

@@ -65,6 +65,9 @@ final class ClientMenu {
findInTree.addActionListener(a -> actions.findClientInTree(client)); findInTree.addActionListener(a -> actions.findClientInTree(client));
menu.add(findInTree); menu.add(findInTree);
} }
JMenuItem description = new JMenuItem("Change Description", Icons.of("EDIT"));
description.addActionListener(a -> actions.changeClientDescription(client));
menu.add(description);
JMenuItem info = new JMenuItem("Connection Info", Icons.of("INFO")); JMenuItem info = new JMenuItem("Connection Info", Icons.of("INFO"));
info.addActionListener(a -> actions.showConnectionInfo(client)); info.addActionListener(a -> actions.showConnectionInfo(client));
menu.add(info); menu.add(info);

View File

@@ -8,6 +8,7 @@ import com.ts3client.net.TeamspeakConnection;
import com.ts3client.text.TsLink; import com.ts3client.text.TsLink;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import java.awt.Component; import java.awt.Component;
/** /**
@@ -201,6 +202,32 @@ final class ServerTabTreeActions implements ServerTreePanel.Actions {
groupsDialog = null; groupsDialog = null;
} }
/**
* Reads the description from the server before prompting, since the model only
* carries one for clients whose info has been looked at.
*/
@Override
public void changeClientDescription(ClientEntry client) {
if (!conn.isConnected()) return;
conn.requestClientDescription(client.id, (description, error) -> SwingUtilities.invokeLater(() -> {
if (error != null) {
JOptionPane.showMessageDialog(host, "Could not read the description: " + error,
"Error", JOptionPane.ERROR_MESSAGE);
return;
}
String edited = (String) JOptionPane.showInputDialog(host,
"Description for " + client.nickname + ":", "Change Description",
JOptionPane.PLAIN_MESSAGE, Icons.of("EDIT"), null, description);
if (edited == null || edited.equals(description)) return;
conn.setClientDescription(client.id, edited, failure -> SwingUtilities.invokeLater(() -> {
if (failure != null) {
JOptionPane.showMessageDialog(host, "Could not change the description: " + failure,
"Error", JOptionPane.ERROR_MESSAGE);
}
}));
}));
}
@Override @Override
public void setChannelSubscribed(ChannelNode channel, boolean family, boolean subscribed) { public void setChannelSubscribed(ChannelNode channel, boolean family, boolean subscribed) {
if (conn.isConnected()) conn.setChannelsSubscribed(channel.familyIds(family), subscribed); if (conn.isConnected()) conn.setChannelsSubscribed(channel.familyIds(family), subscribed);

View File

@@ -99,6 +99,9 @@ public final class ServerTreePanel extends JScrollPane {
/** Opens the full server-groups list dialog for a client. */ /** Opens the full server-groups list dialog for a client. */
void showServerGroupsDialog(ClientEntry client); void showServerGroupsDialog(ClientEntry client);
/** Edits a client's description — our own included. */
void changeClientDescription(ClientEntry client);
} }
private final DropIndicatorTree tree; private final DropIndicatorTree tree;