From b29f518d59f7be7185705baed93d2f13f2e62849 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Tue, 18 Aug 2026 15:25:41 +0000 Subject: [PATCH] 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. --- .../java/com/ts3client/ui/ClientMenu.java | 45 ++++++++++++++++--- .../ts3client/ui/ServerTabTreeActions.java | 2 +- .../com/ts3client/ui/ServerTreePanel.java | 3 +- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ClientMenu.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ClientMenu.java index df88a94..db660aa 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ClientMenu.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ClientMenu.java @@ -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")); - findInTree.addActionListener(a -> actions.findClientInTree(client)); - menu.add(findInTree); + 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"); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java index 50e1d7e..576e249 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java @@ -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); } 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 88fd27f..7ffb016 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 @@ -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) {