Hide/gray client-menu items that can't do anything useful right now

"Find Client in Channel Tree" only makes sense when the menu was
opened somewhere other than the tree itself (chat log links, etc.),
so it's hidden when opened from the tree, and disabled elsewhere if
the client's channel isn't currently subscribed (so the tree
wouldn't actually show them). Adds "Join Channel of Client" above
"Move Client to own Channel", disabled under the same visibility
condition plus a full channel. Both use the CHANNEL_SWITCH icon.

Note: TS3 doesn't expose a channel's join-power threshold as a plain
property (only group membership powers work that way); it's a
permission resolved through channel/channel-group/server-group
inheritance that a regular client can't query, so join-power itself
isn't part of this check.
This commit is contained in:
2026-08-18 15:25:41 +00:00
parent acf0221837
commit b29f518d59
3 changed files with 43 additions and 7 deletions

View File

@@ -1,5 +1,6 @@
package com.ts3client.ui;
import com.ts3client.net.ChannelNode;
import com.ts3client.net.ClientEntry;
import com.ts3client.net.Group;
import com.ts3client.net.ServerModel;
@@ -19,8 +20,13 @@ final class ClientMenu {
private ClientMenu() {
}
static JPopupMenu build(ClientEntry client, boolean self, ServerModel model, GroupIcons groupIcons,
ServerTreePanel.Actions actions) {
/**
* @param fromTree whether this menu was opened from the server tree itself, as
* opposed to a client link elsewhere (chat log, etc.) — "Find
* Client in Channel Tree" is redundant in the former case.
*/
static JPopupMenu build(ClientEntry client, boolean self, boolean fromTree, ServerModel model,
GroupIcons groupIcons, ServerTreePanel.Actions actions) {
JPopupMenu menu = new JPopupMenu();
if (!self) {
JMenuItem pm = new JMenuItem("Open text chat", Icons.of("PLAYER_CHAT"));
@@ -53,14 +59,21 @@ final class ClientMenu {
menu.add(buildServerGroupMenu(client, model, groupIcons, actions));
menu.add(buildChannelGroupMenu(client, model, groupIcons, actions));
menu.addSeparator();
JMenuItem findInTree = new JMenuItem("Find Client in Channel Tree", Icons.of("PLAYER_ON"));
if (!fromTree) {
JMenuItem findInTree = new JMenuItem("Find Client in Channel Tree", Icons.of("CHANNEL_SWITCH"));
findInTree.setEnabled(isVisible(client, model));
findInTree.addActionListener(a -> actions.findClientInTree(client));
menu.add(findInTree);
}
JMenuItem info = new JMenuItem("Connection Info", Icons.of("INFO"));
info.addActionListener(a -> actions.showConnectionInfo(client));
menu.add(info);
if (!self) {
menu.addSeparator();
JMenuItem joinChannel = new JMenuItem("Join Channel of Client", Icons.of("CHANNEL_SWITCH"));
joinChannel.setEnabled(canJoinChannelOf(client, model));
joinChannel.addActionListener(a -> actions.joinChannel(client.channelId));
menu.add(joinChannel);
JMenuItem moveHere = new JMenuItem("Move Client to own Channel", Icons.of("MOVE_CLIENT_TO_OWN_CHANNEL"));
moveHere.addActionListener(a -> actions.moveClientToOwnChannel(client));
menu.add(moveHere);
@@ -68,6 +81,28 @@ final class ClientMenu {
return menu;
}
/**
* 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).
*/
private static boolean isVisible(ClientEntry client, ServerModel model) {
ChannelNode channel = model.getChannel(client.channelId);
return channel != null && channel.subscribed;
}
/**
* Whether we can expect a join to succeed: the channel must be visible to us, and
* not already full. This can't account for a join-power permission requirement —
* unlike group membership, TS3 doesn't expose a channel's join-power threshold as
* a plain property, only as a permission resolved through channel/channel-group/
* server-group inheritance, which isn't something a regular client can query.
*/
private static boolean canJoinChannelOf(ClientEntry client, ServerModel model) {
ChannelNode channel = model.getChannel(client.channelId);
if (channel == null || !channel.subscribed) return false;
return channel.maxClients < 0 || channel.clients.size() < channel.maxClients;
}
private static JMenu buildServerGroupMenu(ClientEntry client, ServerModel model, GroupIcons groupIcons,
ServerTreePanel.Actions actions) {
JMenu menu = new JMenu("Set Server Groups");

View File

@@ -84,7 +84,7 @@ final class ServerTabTreeActions implements ServerTreePanel.Actions {
chatPanel.appendSystem("That client is no longer on the server.");
return;
}
ClientMenu.build(client, client.id == conn.getSelfClientId(), conn.getModel(), groupIcons, this)
ClientMenu.build(client, client.id == conn.getSelfClientId(), false, conn.getModel(), groupIcons, this)
.show(source, x, y);
}

View File

@@ -197,7 +197,8 @@ public final class ServerTreePanel extends JScrollPane {
}
private void showClientMenu(ClientEntry client, MouseEvent e) {
ClientMenu.build(client, client.id == selfClientId, model, groupIcons, actions).show(tree, e.getX(), e.getY());
ClientMenu.build(client, client.id == selfClientId, true, model, groupIcons, actions)
.show(tree, e.getX(), e.getY());
}
private void showChannelMenu(ChannelNode channel, MouseEvent e) {