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 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 10:57:36 +00:00
parent ae8d1f7083
commit f30cecf51c

View File

@@ -18,6 +18,8 @@ import javax.swing.JViewport;
import javax.swing.KeyStroke; import javax.swing.KeyStroke;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import javax.swing.plaf.basic.BasicTreeUI; 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.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath; import javax.swing.tree.TreePath;
@@ -26,7 +28,9 @@ import java.awt.Rectangle;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* The server view: a tree of channels each containing its clients, styled to * 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); 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; private boolean rebuilding;
/** Channels the user collapsed; every other channel is shown open. */
private final Set<Integer> collapsedChannels = new HashSet<>();
private final ServerModel model; private final ServerModel model;
private final GroupIcons groupIcons; private final GroupIcons groupIcons;
private final Actions actions; private final Actions actions;
@@ -225,6 +234,30 @@ public final class ServerTreePanel extends JPanel {
tree.setDropMode(DropMode.ON_OR_INSERT); tree.setDropMode(DropMode.ON_OR_INSERT);
tree.setTransferHandler(new ServerTreeDragAndDrop(model, actions, tree, this::pathOf).transferHandler()); 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 -> { tree.addTreeSelectionListener(e -> {
if (rebuilding) return; if (rebuilding) return;
TreePath path = tree.getSelectionPath(); TreePath path = tree.getSelectionPath();
@@ -364,9 +397,7 @@ public final class ServerTreePanel extends JPanel {
if (node != null) root.add(node); if (node != null) root.add(node);
} }
treeModel.reload(); treeModel.reload();
for (int i = 0; i < tree.getRowCount(); i++) { expandOpenChannels(root, new TreePath(root));
tree.expandRow(i);
}
if (selected != null) { if (selected != null) {
TreePath path = pathOf(selected); TreePath path = pathOf(selected);
if (path != null) tree.setSelectionPath(path); if (path != null) tree.setSelectionPath(path);
@@ -381,6 +412,21 @@ public final class ServerTreePanel extends JPanel {
search.treeRebuilt(); 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() { private Object selectedUserObject() {
TreePath path = tree.getSelectionPath(); TreePath path = tree.getSelectionPath();
return path == null ? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject(); return path == null ? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
@@ -408,6 +454,7 @@ public final class ServerTreePanel extends JPanel {
public void showDisconnected() { public void showDisconnected() {
root.setUserObject("Not connected"); root.setUserObject("Not connected");
root.removeAllChildren(); root.removeAllChildren();
collapsedChannels.clear();
treeModel.reload(); treeModel.reload();
search.treeRebuilt(); search.treeRebuilt();
} }