From f30cecf51c0c0feba3952eec4bddc6958f0189dd Mon Sep 17 00:00:00 2001 From: ericek111 Date: Fri, 25 Sep 2026 10:57:36 +0000 Subject: [PATCH] Keep channels the user collapsed collapsed Every tree rebuild, which any join, leave or talk-power change causes, expanded every row again, so a collapsed channel reopened within seconds. The tree now remembers which channels were collapsed and opens everything else; reopening one restores what is below it. While the find bar filters, everything opens so every match shows. Co-Authored-By: Claude Opus 5.5 --- .../com/ts3client/ui/ServerTreePanel.java | 55 +++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) 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 13be12c..8647930 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 @@ -18,6 +18,8 @@ import javax.swing.JViewport; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.plaf.basic.BasicTreeUI; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeExpansionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; @@ -26,7 +28,9 @@ import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * The server view: a tree of channels each containing its clients, styled to @@ -159,8 +163,13 @@ public final class ServerTreePanel extends JPanel { if (!nickname.isEmpty()) actions.renameSelf(nickname); } }; - /** True while {@link #rebuild()} clears and restores the selection, to swallow the transient null in between. */ + /** + * True while the tree is changed programmatically: a {@link #rebuild()} clearing and restoring + * the selection, or channels being reopened. The listeners ignore what happens meanwhile. + */ private boolean rebuilding; + /** Channels the user collapsed; every other channel is shown open. */ + private final Set collapsedChannels = new HashSet<>(); private final ServerModel model; private final GroupIcons groupIcons; private final Actions actions; @@ -225,6 +234,30 @@ public final class ServerTreePanel extends JPanel { tree.setDropMode(DropMode.ON_OR_INSERT); tree.setTransferHandler(new ServerTreeDragAndDrop(model, actions, tree, this::pathOf).transferHandler()); + tree.addTreeExpansionListener(new TreeExpansionListener() { + @Override + public void treeCollapsed(TreeExpansionEvent e) { + if (rebuilding) return; + Object obj = ((DefaultMutableTreeNode) e.getPath().getLastPathComponent()).getUserObject(); + if (obj instanceof ChannelNode) collapsedChannels.add(((ChannelNode) obj).id); + } + + @Override + public void treeExpanded(TreeExpansionEvent e) { + if (rebuilding) return; + DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.getPath().getLastPathComponent(); + if (!(node.getUserObject() instanceof ChannelNode)) return; + collapsedChannels.remove(((ChannelNode) node.getUserObject()).id); + // A rebuild while it was shut left everything below it shut too. + rebuilding = true; + try { + expandOpenChannels(node, e.getPath()); + } finally { + rebuilding = false; + } + } + }); + tree.addTreeSelectionListener(e -> { if (rebuilding) return; TreePath path = tree.getSelectionPath(); @@ -364,9 +397,7 @@ public final class ServerTreePanel extends JPanel { if (node != null) root.add(node); } treeModel.reload(); - for (int i = 0; i < tree.getRowCount(); i++) { - tree.expandRow(i); - } + expandOpenChannels(root, new TreePath(root)); if (selected != null) { TreePath path = pathOf(selected); if (path != null) tree.setSelectionPath(path); @@ -381,6 +412,21 @@ public final class ServerTreePanel extends JPanel { search.treeRebuilt(); } + /** + * Opens {@code node} and every channel below it, except the ones the user collapsed and + * whatever is below those. While the find bar filters, everything opens, so every match shows. + */ + private void expandOpenChannels(DefaultMutableTreeNode node, TreePath path) { + tree.expandPath(path); + boolean filtering = search.isFiltering(); + for (int i = 0; i < node.getChildCount(); i++) { + DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt(i); + if (child.isLeaf() || !(child.getUserObject() instanceof ChannelNode)) continue; + if (!filtering && collapsedChannels.contains(((ChannelNode) child.getUserObject()).id)) continue; + expandOpenChannels(child, path.pathByAddingChild(child)); + } + } + private Object selectedUserObject() { TreePath path = tree.getSelectionPath(); return path == null ? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject(); @@ -408,6 +454,7 @@ public final class ServerTreePanel extends JPanel { public void showDisconnected() { root.setUserObject("Not connected"); root.removeAllChildren(); + collapsedChannels.clear(); treeModel.reload(); search.treeRebuilt(); }