Log server activity, scrollable chat tabs, and move info panel to a chat tab
Native TS3 logs client joins/leaves/moves, group changes and channel edits to the server tab; minor local-only notices (mic mute, tray) no longer spam the log. Chat tabs scroll by mouse wheel like server tabs. The info panel can now be moved into a persistent, generically-labelled chat tab that tracks tree selection and collapses the panel's space entirely; a tree rebuild no longer briefly fires a null selection that reset this. Added "Find Client in Channel Tree" to the client context menu, and tree selection now survives model rebuilds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,9 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
|
||||
boolean isClientLocallyMuted(int clientId);
|
||||
|
||||
/** Selects and scrolls to a client in the tree, e.g. from a chat/log link. */
|
||||
void findClientInTree(ClientEntry client);
|
||||
|
||||
/** A channel or client node was selected (or {@code null} when cleared). */
|
||||
void onSelectionChanged(Object userObject);
|
||||
|
||||
@@ -95,6 +98,8 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
private final DropIndicatorTree tree;
|
||||
private final DefaultMutableTreeNode root = new DefaultMutableTreeNode();
|
||||
private final DefaultTreeModel treeModel = new DefaultTreeModel(root);
|
||||
/** True while {@link #rebuild()} clears and restores the selection, to swallow the transient null in between. */
|
||||
private boolean rebuilding;
|
||||
private final ServerModel model;
|
||||
private final GroupIcons groupIcons;
|
||||
private final Actions actions;
|
||||
@@ -133,6 +138,7 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
tree.setTransferHandler(new TreeTransferHandler());
|
||||
|
||||
tree.addTreeSelectionListener(e -> {
|
||||
if (rebuilding) return;
|
||||
TreePath path = tree.getSelectionPath();
|
||||
Object obj = null;
|
||||
if (path != null) {
|
||||
@@ -172,6 +178,16 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
this.selfClientId = id;
|
||||
}
|
||||
|
||||
/** Selects and scrolls to the row showing {@code client}, if it is currently visible. */
|
||||
public void selectClient(int clientId) {
|
||||
ClientEntry client = model.getClient(clientId);
|
||||
if (client == null) return;
|
||||
TreePath path = pathOf(client);
|
||||
if (path == null) return;
|
||||
tree.setSelectionPath(path);
|
||||
tree.scrollPathToVisible(path);
|
||||
}
|
||||
|
||||
private Object nodeAt(MouseEvent e) {
|
||||
TreePath path = tree.getPathForLocation(e.getX(), e.getY());
|
||||
if (path == null) return null;
|
||||
@@ -542,18 +558,43 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
}
|
||||
}
|
||||
|
||||
/** Rebuilds the tree from the model, preserving full expansion. */
|
||||
/**
|
||||
* Rebuilds the tree from the model, preserving full expansion. Rebuilding
|
||||
* replaces every tree node, which would otherwise drop the current
|
||||
* selection on every update (a client talking, a group change, …); the
|
||||
* previously selected channel/client is restored by identity once the new
|
||||
* nodes are in place, so a selection sticks until the user changes it.
|
||||
*/
|
||||
public void rebuild() {
|
||||
root.setUserObject(model.getServerName());
|
||||
root.removeAllChildren();
|
||||
List<ChannelNode> roots = model.buildTree();
|
||||
for (ChannelNode c : roots) {
|
||||
root.add(buildChannel(c));
|
||||
}
|
||||
treeModel.reload();
|
||||
for (int i = 0; i < tree.getRowCount(); i++) {
|
||||
tree.expandRow(i);
|
||||
Object selected = selectedUserObject();
|
||||
rebuilding = true;
|
||||
try {
|
||||
root.setUserObject(model.getServerName());
|
||||
root.removeAllChildren();
|
||||
List<ChannelNode> roots = model.buildTree();
|
||||
for (ChannelNode c : roots) {
|
||||
root.add(buildChannel(c));
|
||||
}
|
||||
treeModel.reload();
|
||||
for (int i = 0; i < tree.getRowCount(); i++) {
|
||||
tree.expandRow(i);
|
||||
}
|
||||
if (selected != null) {
|
||||
TreePath path = pathOf(selected);
|
||||
if (path != null) tree.setSelectionPath(path);
|
||||
}
|
||||
} finally {
|
||||
rebuilding = false;
|
||||
}
|
||||
// The selection listener was swallowed above; tell the caller only if it actually changed
|
||||
// (e.g. the previously selected channel/client is gone), since it already knows the rest.
|
||||
Object nowSelected = selectedUserObject();
|
||||
if (nowSelected != selected) actions.onSelectionChanged(nowSelected);
|
||||
}
|
||||
|
||||
private Object selectedUserObject() {
|
||||
TreePath path = tree.getSelectionPath();
|
||||
return path == null ? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
|
||||
}
|
||||
|
||||
private DefaultMutableTreeNode buildChannel(ChannelNode c) {
|
||||
|
||||
Reference in New Issue
Block a user