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) {