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:
2026-08-17 21:47:12 +00:00
parent 047c89404c
commit 71084ea309
9 changed files with 439 additions and 35 deletions

View File

@@ -39,6 +39,9 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions {
private final ChatPanel chatPanel;
private final InfoPanel infoPanel = new InfoPanel();
private final JComponent component;
private JSplitPane leftColumn;
private int normalDividerSize;
private int savedDividerLocation = -1;
/** Label shown in the tab bar: the server name once known, the address before that. */
private String title = "New connection";
@@ -82,9 +85,27 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions {
});
chatPanel.setInputEnabled(false);
JSplitPane leftColumn = new JSplitPane(JSplitPane.VERTICAL_SPLIT, treePanel, infoPanel);
infoPanel.setDescriptionHandler(new InfoPanel.DescriptionHandler() {
@Override
public void showInfoTab(String html, Runnable onClose) {
chatPanel.openDescriptionTab("info", Icons.channelClientPair(), "", html, onClose);
}
@Override
public void closeInfoTab() {
chatPanel.closeNoteTab("info");
}
@Override
public void setPanelHidden(boolean hidden) {
ServerTab.this.setInfoPanelHidden(hidden);
}
});
leftColumn = new JSplitPane(JSplitPane.VERTICAL_SPLIT, treePanel, infoPanel);
leftColumn.setResizeWeight(0.68);
leftColumn.setContinuousLayout(true);
normalDividerSize = leftColumn.getDividerSize();
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftColumn, chatPanel);
split.setResizeWeight(0.55);
@@ -234,21 +255,18 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions {
void setMicMuted(boolean muted) {
micMuted = muted;
conn.setMicMuted(muted);
chatPanel.appendSystem(muted ? "Microphone muted." : "Microphone active.");
}
/** TS3's "Local Mic Mute": silences capture without publishing a status change. */
void setMicLocalMuted(boolean muted) {
micLocalMuted = muted;
conn.setMicLocalMuted(muted);
chatPanel.appendSystem(muted ? "Microphone locally muted." : "Microphone locally unmuted.");
}
void setDeafened(boolean deaf) {
deafened = deaf;
conn.setDeafened(deaf);
if (deaf) micMuted = true;
chatPanel.appendSystem(deaf ? "Speakers muted (deafened)." : "Speakers active.");
}
/** Hands the capture device to (or takes it from) this connection. */
@@ -264,8 +282,6 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions {
this.away = away;
if (message != null) this.awayMessage = message;
conn.setAway(away, awayMessage);
chatPanel.appendSystem(!away ? "No longer away."
: awayMessage.isEmpty() ? "Away." : "Away: " + awayMessage);
}
void setCommander(boolean commander) {
@@ -425,7 +441,12 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions {
if (conn.getPlayback() == null) return;
boolean now = !conn.getPlayback().isClientMuted(client.id);
conn.getPlayback().setClientMuted(client.id, now);
chatPanel.appendSystem((now ? "Muted " : "Unmuted ") + client.nickname + ".");
}
@Override
public void findClientInTree(ClientEntry client) {
host.selectTab(this);
treePanel.selectClient(client.id);
}
@Override
@@ -481,6 +502,22 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions {
}
}
/** Collapses the info panel to nothing while its content lives in the chat tab, or restores it. */
private void setInfoPanelHidden(boolean hidden) {
if (hidden == !infoPanel.isVisible()) return;
if (hidden) {
savedDividerLocation = leftColumn.getDividerLocation();
infoPanel.setVisible(false);
leftColumn.setDividerSize(0);
leftColumn.setDividerLocation(1.0);
} else {
infoPanel.setVisible(true);
leftColumn.setDividerSize(normalDividerSize);
if (savedDividerLocation >= 0) leftColumn.setDividerLocation(savedDividerLocation);
}
leftColumn.revalidate();
}
// ---- ConnectionListener (marshal to EDT) ----
@Override
@@ -518,6 +555,7 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions {
infoPanel.clear();
chatPanel.setInputEnabled(false);
chatPanel.closePrivateChats();
chatPanel.closeNoteTabs();
chatPanel.appendSystem("Disconnected" + (reason == null || reason.isEmpty() ? "." : ": " + reason));
host.tabDisconnected(this);
});
@@ -581,6 +619,11 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions {
});
}
@Override
public void onServerLog(String message) {
SwingUtilities.invokeLater(() -> chatPanel.appendServerLog(message));
}
@Override
public void onPoke(String fromName, String message) {
SwingUtilities.invokeLater(() -> {