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 7b03b6e..40540e5 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 @@ -6,6 +6,7 @@ public final class ClientEntry { public int channelId; public String nickname; public String uniqueId = ""; + public int databaseId; public int type; // 0 = normal voice client, 1 = server-query public int talkPower; @@ -39,4 +40,28 @@ public final class ClientEntry { public boolean isQuery() { return type == 1; } + + /** Adds a server group id, if not already present. */ + public void addServerGroup(int groupId) { + for (int id : serverGroupIds) if (id == groupId) return; + int[] updated = java.util.Arrays.copyOf(serverGroupIds, serverGroupIds.length + 1); + updated[serverGroupIds.length] = groupId; + serverGroupIds = updated; + } + + /** Removes a server group id, if present. */ + public void removeServerGroup(int groupId) { + int index = -1; + for (int i = 0; i < serverGroupIds.length; i++) { + if (serverGroupIds[i] == groupId) { + index = i; + break; + } + } + if (index < 0) return; + int[] updated = new int[serverGroupIds.length - 1]; + System.arraycopy(serverGroupIds, 0, updated, 0, index); + System.arraycopy(serverGroupIds, index + 1, updated, index, serverGroupIds.length - index - 1); + serverGroupIds = updated; + } } diff --git a/ts3-client/core/src/main/java/com/ts3client/net/ConnectionEventHandler.java b/ts3-client/core/src/main/java/com/ts3client/net/ConnectionEventHandler.java index 8c91930..bfec351 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/ConnectionEventHandler.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/ConnectionEventHandler.java @@ -43,6 +43,7 @@ final class ConnectionEventHandler implements TS3Listener { c.away = e.isClientAway(); c.awayMessage = TeamspeakConnection.orEmpty(e.get("client_away_message")); c.uniqueId = TeamspeakConnection.orEmpty(e.getUniqueClientIdentifier()); + c.databaseId = e.getClientDatabaseId(); c.serverGroupIds = parseIntList(e.getClientServerGroups()); c.channelGroupId = e.getClientChannelGroupId(); c.self = (e.getClientId() == conn.getSelfClientId()); @@ -423,6 +424,8 @@ final class ConnectionEventHandler implements TS3Listener { @Override public void onServerGroupClientAdded(ServerGroupClientAddedEvent e) { + ClientEntry c = conn.getModel().getClient(e.getClientId()); + if (c != null) c.addServerGroup(e.getServerGroupId()); boolean self = e.getClientId() == conn.getSelfClientId(); conn.sound(self ? byInvoker(e, SoundEvent.YOU_SERVERGROUP_ADDED_BY_USER, SoundEvent.YOU_SERVERGROUP_ADDED_BY_USER, @@ -432,11 +435,14 @@ final class ConnectionEventHandler implements TS3Listener { groupVars(e.getClientId(), e.getName())); conn.log(clientLogName(e.getClientId()) + " was added to server group \"" + e.getName() + "\" by " + invokerName(e) + "."); + conn.ui.onModelChanged(); } @Override public void onServerGroupClientDeleted(ServerGroupClientDeletedEvent e) { int clientId = safeInt(e, "clid"); + ClientEntry c = conn.getModel().getClient(clientId); + if (c != null) c.removeServerGroup(e.getServerGroupId()); boolean self = clientId == conn.getSelfClientId(); conn.sound(self ? byInvoker(e, SoundEvent.YOU_SERVERGROUP_REMOVED_BY_USER, @@ -446,6 +452,7 @@ final class ConnectionEventHandler implements TS3Listener { groupVars(clientId, e.get("name"))); conn.log(clientLogName(clientId) + " was removed from server group \"" + TeamspeakConnection.orEmpty(e.get("name")) + "\" by " + invokerName(e) + "."); + conn.ui.onModelChanged(); } @Override @@ -509,7 +516,18 @@ final class ConnectionEventHandler implements TS3Listener { } private static Group toGroup(BaseEvent e, int id) { - return new Group(id, e.get("name"), TeamspeakConnection.safeLong(e, "iconid"), safeInt(e, "sortid")); + return new Group(id, e.get("name"), TeamspeakConnection.safeLong(e, "iconid"), safeInt(e, "sortid"), + safeInt(e, "type"), safeInt(e, "n_member_addp"), safeInt(e, "n_member_removep")); + } + + @Override + public void onPermissionList(PermissionListEvent e) { + conn.getModel().putPermissionName(e.get("permname")); + } + + @Override + public void onClientNeededPermissions(ClientNeededPermissionsEvent e) { + conn.getModel().putSelfPermissionValue(safeInt(e, "permid"), safeInt(e, "permvalue")); } @Override diff --git a/ts3-client/core/src/main/java/com/ts3client/net/Group.java b/ts3-client/core/src/main/java/com/ts3client/net/Group.java index 4865653..a81b397 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/Group.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/Group.java @@ -9,11 +9,26 @@ public final class Group { public final long iconId; /** Display order among groups; lower comes first. */ public final int sortId; + /** TS3 group type: 0 = template, 1 = regular, 2 = query (server groups only). */ + public final int type; + /** Power needed to add a member to this group. */ + public final int neededMemberAddPower; + /** Power needed to remove a member from this group. */ + public final int neededMemberRemovePower; - public Group(int id, String name, long iconId, int sortId) { + public Group(int id, String name, long iconId, int sortId, int type, + int neededMemberAddPower, int neededMemberRemovePower) { this.id = id; this.name = name; this.iconId = iconId; this.sortId = sortId; + this.type = type; + this.neededMemberAddPower = neededMemberAddPower; + this.neededMemberRemovePower = neededMemberRemovePower; + } + + /** Regular, user-assignable groups exclude templates and query-only groups. */ + public boolean isRegular() { + return type == 1; } } diff --git a/ts3-client/core/src/main/java/com/ts3client/net/ServerModel.java b/ts3-client/core/src/main/java/com/ts3client/net/ServerModel.java index ed62de2..1832325 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/ServerModel.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/ServerModel.java @@ -18,13 +18,29 @@ public final class ServerModel { private final Map clients = new LinkedHashMap<>(); private final Map serverGroups = new LinkedHashMap<>(); private final Map channelGroups = new LinkedHashMap<>(); + /** + * Permission id -> name, learned from the server's {@code permissionlist} response. + * Entries carry no id of their own: TS3 numbers them by position in the response, + * and the empty "group_id_end" separator records marking category boundaries don't + * count toward that position, so only {@link #putPermissionName} advances it. + */ + private final Map permissionNames = new LinkedHashMap<>(); + private int nextPermissionId; + /** Permission id -> the local client's resolved value, from {@code notifyclientneededpermissions}. */ + private final Map selfPermissionValues = new LinkedHashMap<>(); private String serverName = "TeamSpeak Server"; + /** The channel group everyone starts in, e.g. "Guest" — not worth offering to (re)assign. */ + private int defaultChannelGroupId; public synchronized void clear() { channels.clear(); clients.clear(); serverGroups.clear(); channelGroups.clear(); + permissionNames.clear(); + nextPermissionId = 0; + selfPermissionValues.clear(); + defaultChannelGroupId = 0; } // ---- groups ---- @@ -78,6 +94,54 @@ public final class ServerModel { return g == null ? null : g.name; } + /** Regular (non-template, non-query) server groups, ordered for display. */ + public synchronized List allServerGroups() { + return regularGroups(serverGroups); + } + + /** Regular (non-template, non-query) channel groups, ordered for display. */ + public synchronized List allChannelGroups() { + return regularGroups(channelGroups); + } + + private static List regularGroups(Map groups) { + List list = new ArrayList<>(); + for (Group g : groups.values()) { + if (g.isRegular()) list.add(g); + } + list.sort(Comparator.comparingInt((Group g) -> g.sortId).thenComparingInt(g -> g.id)); + return list; + } + + // ---- permissions ---- + + /** Records the next named entry of a {@code permissionlist} response; ignores separators. */ + public synchronized void putPermissionName(String name) { + if (name == null || name.isEmpty()) return; + permissionNames.put(nextPermissionId, name); + nextPermissionId++; + } + + public synchronized void putSelfPermissionValue(int permId, int value) { + selfPermissionValues.put(permId, value); + } + + /** + * The local client's resolved value for a named permission (e.g. + * {@code "i_group_needed_member_add_power"}), or {@code 0} if it isn't known + * yet (the {@code permissionlist} request is still in flight, or the server + * never reported a non-default value for it). + */ + public synchronized int selfPermissionValue(String name) { + for (Map.Entry entry : permissionNames.entrySet()) { + if (entry.getValue().equals(name)) { + Integer value = selfPermissionValues.get(entry.getKey()); + if (value != null) return value; + } + } + return 0; + } + public synchronized String getServerName() { return serverName; } @@ -86,6 +150,14 @@ public final class ServerModel { if (name != null && !name.isEmpty()) this.serverName = name; } + public synchronized int defaultChannelGroupId() { + return defaultChannelGroupId; + } + + public synchronized void setDefaultChannelGroupId(int id) { + this.defaultChannelGroupId = id; + } + // ---- channels ---- public synchronized ChannelNode putChannel(int id, String name, int parentId, int order) { 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 d994a5b..62fe48f 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 @@ -264,6 +264,7 @@ public final class TeamspeakConnection implements TS3Listener { disconnectAnnounced = false; deafened = false; ui.onConnected(); + requestPermissionNames(); ui.onStatus("Retrieving channels…"); syncAll(); @@ -396,6 +397,8 @@ public final class TeamspeakConnection implements TS3Listener { } // The name comes with initserver; servergetvariables never reports it. model.setServerName(client.getServerName()); + model.setDefaultChannelGroupId( + (int) safeLong(client.getServerProperties().get("virtualserver_default_channel_group"))); try { for (Channel ch : client.listChannels()) { model.putChannel(ch.getId(), ch.getName(), ch.getParentChannelId(), ch.getOrder()); @@ -647,6 +650,51 @@ public final class TeamspeakConnection implements TS3Listener { }, "ts3j-move-channel").start(); } + /** + * Asks the server to (re-)send its permission definitions, which arrive as a burst + * of {@code notifypermissionlist} events — the only way to learn permission names, + * since {@code notifyclientneededpermissions} (which resolves our own power for + * them) only ever reports numeric ids. Needed for {@link ServerModel#selfPermissionValue} + * to work; harmless if it's slow or fails, since group-assignment eligibility just + * won't be known yet. + */ + private void requestPermissionNames() { + new Thread(() -> { + try { + client.executeCommand(new SingleCommand("permissionlist", ProtocolRole.CLIENT)).complete(); + } catch (Exception ignored) { + // Best-effort: the menus fall back to treating unresolved permissions as 0. + } + }, "ts3j-permission-list").start(); + } + + /** Assigns or removes a server group for a client (by database id, as {@code servergroupaddclient} needs). */ + public void setClientServerGroup(int clientDatabaseId, int groupId, boolean assign) { + new Thread(() -> { + try { + if (assign) client.serverGroupAddClient(groupId, clientDatabaseId); + else client.serverGroupRemoveClient(groupId, clientDatabaseId); + } catch (Exception e) { + error("Could not " + (assign ? "assign" : "remove") + " server group: " + rootMessage(e)); + } + }, "ts3j-server-group").start(); + } + + /** Assigns a channel group for a client in the channel it currently sits in. */ + public void setClientChannelGroup(int clientDatabaseId, int channelId, int groupId) { + new Thread(() -> { + try { + SingleCommand cmd = new SingleCommand("setclientchannelgroup", ProtocolRole.CLIENT); + cmd.add(new CommandSingleParameter("cgid", Integer.toString(groupId))); + cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId))); + cmd.add(new CommandSingleParameter("cldbid", Integer.toString(clientDatabaseId))); + client.executeCommand(cmd).complete(); + } catch (Exception e) { + error("Could not set channel group: " + rootMessage(e)); + } + }, "ts3j-channel-group").start(); + } + /** * Subscribes to (or unsubscribes from) a set of channels in one command. The model * is left alone: the server answers with the subscription events that update it. 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 9bf7a4f..df88a94 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,9 +1,14 @@ package com.ts3client.ui; import com.ts3client.net.ClientEntry; +import com.ts3client.net.Group; +import com.ts3client.net.ServerModel; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; +import java.util.List; /** * The context menu for a client, shared by the server tree and the clickable @@ -14,7 +19,8 @@ final class ClientMenu { private ClientMenu() { } - static JPopupMenu build(ClientEntry client, boolean self, ServerTreePanel.Actions actions) { + static JPopupMenu build(ClientEntry client, boolean self, ServerModel model, GroupIcons groupIcons, + ServerTreePanel.Actions actions) { JPopupMenu menu = new JPopupMenu(); if (!self) { JMenuItem pm = new JMenuItem("Open text chat", Icons.of("PLAYER_CHAT")); @@ -44,6 +50,9 @@ final class ClientMenu { menu.add(me); } menu.addSeparator(); + 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); @@ -58,4 +67,68 @@ final class ClientMenu { } return menu; } + + private static JMenu buildServerGroupMenu(ClientEntry client, ServerModel model, GroupIcons groupIcons, + ServerTreePanel.Actions actions) { + JMenu menu = new JMenu("Set Server Groups"); + menu.setIcon(Icons.of("PERMISSIONS_SERVER_GROUPS")); + JMenuItem dialog = new JMenuItem("Server Groups Dialog..."); + dialog.addActionListener(a -> actions.showServerGroupsDialog(client)); + menu.add(dialog); + menu.addSeparator(); + + int addPower = model.selfPermissionValue("i_group_needed_member_add_power"); + int removePower = model.selfPermissionValue("i_group_needed_member_remove_power"); + for (Group g : model.allServerGroups()) { + boolean assigned = contains(client.serverGroupIds, g.id); + if (!canAssign(g, assigned, addPower, removePower)) continue; + JCheckBoxMenuItem item = new JCheckBoxMenuItem(g.name, groupIcons.iconOf(g)); + item.setSelected(assigned); + item.addActionListener(a -> actions.setClientServerGroup(client, g, !assigned)); + menu.add(item); + } + return menu; + } + + private static JMenu buildChannelGroupMenu(ClientEntry client, ServerModel model, GroupIcons groupIcons, + ServerTreePanel.Actions actions) { + JMenu menu = new JMenu("Set Channel Group"); + menu.setIcon(Icons.of("PERMISSIONS_CHANNEL_GROUPS")); + int addPower = model.selfPermissionValue("i_group_needed_member_add_power"); + int removePower = model.selfPermissionValue("i_group_needed_member_remove_power"); + int defaultGroupId = model.defaultChannelGroupId(); + for (Group g : model.allChannelGroups()) { + if (g.id == defaultGroupId) continue; + boolean assigned = client.channelGroupId == g.id; + if (!canAssign(g, assigned, addPower, removePower)) continue; + JCheckBoxMenuItem item = new JCheckBoxMenuItem(g.name, groupIcons.iconOf(g)); + item.setSelected(assigned); + item.addActionListener(a -> actions.setClientChannelGroup(client, g)); + menu.add(item); + } + return menu; + } + + /** + * Whether the local client has enough power to (un)assign this group: the add + * power always gates the checkbox, and the remove power additionally gates + * unassigning an already-held group. {@code -1} is TS3's "unlimited" sentinel on + * either side: an unlimited local power always passes, and a group that needs + * unlimited power can only be touched by a local client that has it. + */ + static boolean canAssign(Group g, boolean assigned, int addPower, int removePower) { + return hasPower(g.neededMemberAddPower, addPower) + && (!assigned || hasPower(g.neededMemberRemovePower, removePower)); + } + + private static boolean hasPower(int needed, int own) { + if (own == -1) return true; + if (needed == -1) return false; + return own >= needed; + } + + private static boolean contains(int[] ids, int id) { + for (int i : ids) if (i == id) return true; + return false; + } } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerGroupsDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerGroupsDialog.java new file mode 100644 index 0000000..5d0c412 --- /dev/null +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerGroupsDialog.java @@ -0,0 +1,117 @@ +package com.ts3client.ui; + +import com.ts3client.net.ClientEntry; +import com.ts3client.net.Group; +import com.ts3client.net.TeamspeakConnection; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.GrayFilter; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingConstants; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Every regular server group on the server, as a scrollable checkbox list. Groups + * the local client has no power to (un)assign are shown too, so the client's full + * membership is visible, but greyed out and disabled. + */ +final class ServerGroupsDialog extends JDialog { + + private final TeamspeakConnection conn; + private final GroupIcons groupIcons; + private final int clientId; + private final ServerTreePanel.Actions actions; + + private final JPanel list = new JPanel(); + private final JLabel header = new JLabel(); + /** Cached grayscale icons, keyed by the source icon's identity. */ + private final Map grayscale = new HashMap<>(); + + ServerGroupsDialog(MainFrame owner, TeamspeakConnection conn, GroupIcons groupIcons, + ClientEntry client, ServerTreePanel.Actions actions) { + super(owner, "Server Groups", true); + this.conn = conn; + this.groupIcons = groupIcons; + this.clientId = client.id; + this.actions = actions; + + list.setLayout(new BoxLayout(list, BoxLayout.Y_AXIS)); + JScrollPane scroll = new JScrollPane(list); + scroll.setPreferredSize(new Dimension(280, 360)); + scroll.getVerticalScrollBar().setUnitIncrement(16); + + header.setBorder(BorderFactory.createEmptyBorder(8, 10, 4, 10)); + header.setHorizontalAlignment(SwingConstants.LEFT); + + JButton close = new JButton("Close"); + close.addActionListener(a -> dispose()); + JPanel buttons = new JPanel(); + buttons.add(close); + getRootPane().setDefaultButton(close); + + getContentPane().setLayout(new BorderLayout()); + getContentPane().add(header, BorderLayout.NORTH); + getContentPane().add(scroll, BorderLayout.CENTER); + getContentPane().add(buttons, BorderLayout.SOUTH); + + Dialogs.closeOnEscape(this); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + refresh(); + pack(); + setLocationRelativeTo(owner); + } + + /** Rebuilds the list from the current model state; closes the dialog if the client left. */ + void refresh() { + ClientEntry client = conn.getModel().getClient(clientId); + if (client == null) { + dispose(); + return; + } + header.setText(client.nickname); + + int addPower = conn.getModel().selfPermissionValue("i_group_needed_member_add_power"); + int removePower = conn.getModel().selfPermissionValue("i_group_needed_member_remove_power"); + List groups = conn.getModel().allServerGroups(); + + list.removeAll(); + for (Group g : groups) { + boolean assigned = contains(client.serverGroupIds, g.id); + boolean allowed = ClientMenu.canAssign(g, assigned, addPower, removePower); + + ImageIcon icon = groupIcons.iconOf(g); + JCheckBox box = new JCheckBox(g.name, allowed ? icon : grayscale(icon)); + box.setSelected(assigned); + box.setEnabled(allowed); + box.setAlignmentX(0f); + if (allowed) { + box.addActionListener(a -> actions.setClientServerGroup(client, g, box.isSelected())); + } + list.add(box); + } + list.revalidate(); + list.repaint(); + } + + private ImageIcon grayscale(ImageIcon icon) { + if (icon == null) return null; + return grayscale.computeIfAbsent(icon, + i -> new ImageIcon(GrayFilter.createDisabledImage(i.getImage()))); + } + + private static boolean contains(int[] ids, int id) { + for (int i : ids) if (i == id) return true; + return false; + } +} 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 3a41f84..d47c4d0 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 @@ -69,7 +69,7 @@ final class ServerTab implements ServerTabConnectionEvents.Listener { this.groupIcons = new GroupIcons(conn.getIcons()); this.selfState = new ServerTabSelfState(conn); this.chatPanel = new ChatPanel(); - this.treeActions = new ServerTabTreeActions(host, this, conn, chatPanel, infoPanel); + this.treeActions = new ServerTabTreeActions(host, this, conn, chatPanel, infoPanel, groupIcons); this.treePanel = new ServerTreePanel(conn.getModel(), groupIcons, treeActions); treeActions.attach(treePanel); events.attach(conn, treePanel, chatPanel, selfState, treeActions); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabConnectionEvents.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabConnectionEvents.java index 0545aec..dd2528e 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabConnectionEvents.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabConnectionEvents.java @@ -90,6 +90,7 @@ final class ServerTabConnectionEvents implements ConnectionListener { SwingUtilities.invokeLater(() -> { treePanel.rebuild(); treeActions.renderInfo(); + treeActions.refreshGroupsDialog(); String name = conn.getModel().getServerName(); if (conn.isConnected() && name != null && !name.isBlank() && !name.equals(tab.title())) { listener.setTitle(name); 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 4142699..50e1d7e 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 @@ -2,6 +2,7 @@ package com.ts3client.ui; import com.ts3client.net.ChannelNode; import com.ts3client.net.ClientEntry; +import com.ts3client.net.Group; import com.ts3client.net.TeamspeakConnection; import com.ts3client.text.TsLink; @@ -24,23 +25,32 @@ final class ServerTabTreeActions implements ServerTreePanel.Actions { private final TeamspeakConnection conn; private final ChatPanel chatPanel; private final InfoPanel infoPanel; + private final GroupIcons groupIcons; private ServerTreePanel treePanel; private Object currentSelection; + /** The one open {@link ServerGroupsDialog}, if any, refreshed whenever the model changes. */ + private ServerGroupsDialog groupsDialog; ServerTabTreeActions(MainFrame host, ServerTab tab, TeamspeakConnection conn, - ChatPanel chatPanel, InfoPanel infoPanel) { + ChatPanel chatPanel, InfoPanel infoPanel, GroupIcons groupIcons) { this.host = host; this.tab = tab; this.conn = conn; this.chatPanel = chatPanel; this.infoPanel = infoPanel; + this.groupIcons = groupIcons; } void attach(ServerTreePanel treePanel) { this.treePanel = treePanel; } + /** Called by {@link ServerTabConnectionEvents} whenever the model changes, to live-refresh an open dialog. */ + void refreshGroupsDialog() { + if (groupsDialog != null) groupsDialog.refresh(); + } + /** Refreshes the info panel for whatever is currently selected (or clears it). */ void renderInfo() { Object sel = currentSelection; @@ -74,7 +84,8 @@ final class ServerTabTreeActions implements ServerTreePanel.Actions { chatPanel.appendSystem("That client is no longer on the server."); return; } - ClientMenu.build(client, client.id == conn.getSelfClientId(), this).show(source, x, y); + ClientMenu.build(client, client.id == conn.getSelfClientId(), conn.getModel(), groupIcons, this) + .show(source, x, y); } void handleChannelLink(TsLink.Ref ref, Component source, int x, int y) { @@ -171,6 +182,24 @@ final class ServerTabTreeActions implements ServerTreePanel.Actions { if (self != null) conn.moveClient(client.id, self.channelId, null); } + @Override + public void setClientServerGroup(ClientEntry client, Group group, boolean assign) { + if (conn.isConnected()) conn.setClientServerGroup(client.databaseId, group.id, assign); + } + + @Override + public void setClientChannelGroup(ClientEntry client, Group group) { + if (conn.isConnected()) conn.setClientChannelGroup(client.databaseId, client.channelId, group.id); + } + + @Override + public void showServerGroupsDialog(ClientEntry client) { + if (!conn.isConnected()) return; + groupsDialog = new ServerGroupsDialog(host, conn, groupIcons, client, this); + groupsDialog.setVisible(true); + groupsDialog = null; + } + @Override public void setChannelSubscribed(ChannelNode channel, boolean family, boolean subscribed) { if (conn.isConnected()) conn.setChannelsSubscribed(channel.familyIds(family), subscribed); 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 33968af..88fd27f 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 @@ -2,6 +2,7 @@ package com.ts3client.ui; import com.ts3client.net.ChannelNode; import com.ts3client.net.ClientEntry; +import com.ts3client.net.Group; import com.ts3client.net.ServerModel; import javax.swing.DropMode; @@ -71,6 +72,15 @@ public final class ServerTreePanel extends JScrollPane { * siblings, or 0 to place it first */ void moveChannel(ChannelNode channel, int newParentId, int orderPredecessorId); + + /** Assigns or removes a server group for a client. */ + void setClientServerGroup(ClientEntry client, Group group, boolean assign); + + /** Assigns a channel group for a client in its current channel. */ + void setClientChannelGroup(ClientEntry client, Group group); + + /** Opens the full server-groups list dialog for a client. */ + void showServerGroupsDialog(ClientEntry client); } private final DropIndicatorTree tree; @@ -187,7 +197,7 @@ public final class ServerTreePanel extends JScrollPane { } private void showClientMenu(ClientEntry client, MouseEvent e) { - ClientMenu.build(client, client.id == selfClientId, actions).show(tree, e.getX(), e.getY()); + ClientMenu.build(client, client.id == selfClientId, model, groupIcons, actions).show(tree, e.getX(), e.getY()); } private void showChannelMenu(ChannelNode channel, MouseEvent e) {