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

@@ -20,7 +20,27 @@ import java.util.List;
*/
public final class InfoPanel extends JScrollPane {
/** Moves the whole info panel's content out to a persistent chat tab. */
public interface DescriptionHandler {
/** Shows/updates {@code html} in the info chat tab, creating it if needed. */
void showInfoTab(String html, Runnable onClose);
/** Closes the info chat tab (the user chose to show details in the panel again). */
void closeInfoTab();
/** Hides (or restores) the panel itself, freeing its space, while its content lives in the chat tab. */
void setPanelHidden(boolean hidden);
}
private static final String TOGGLE_HREF = "app:toggle-info-tab";
private final JEditorPane pane = new JEditorPane();
private DescriptionHandler descriptionHandler;
private ChannelNode shownChannel;
private ClientEntry shownClient;
private ServerModel shownModel;
private IconRepository shownIcons;
private boolean inChatTab;
public InfoPanel() {
javax.swing.text.html.HTMLEditorKit kit = new javax.swing.text.html.HTMLEditorKit();
@@ -39,7 +59,12 @@ public final class InfoPanel extends JScrollPane {
pane.setBackground(Theme.CHAT_BG);
pane.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8));
pane.addHyperlinkListener(e -> {
if (e.getEventType() == javax.swing.event.HyperlinkEvent.EventType.ACTIVATED) {
if (e.getEventType() != javax.swing.event.HyperlinkEvent.EventType.ACTIVATED) return;
if (TOGGLE_HREF.equals(e.getDescription())) {
inChatTab = !inChatTab;
if (!inChatTab && descriptionHandler != null) descriptionHandler.closeInfoTab();
render();
} else {
Links.open(e.getDescription(), this);
}
});
@@ -49,10 +74,54 @@ public final class InfoPanel extends JScrollPane {
}
public void clear() {
setHtml("<i style='color:#8a8a8a'>Select a channel or client to see details.</i>");
shownChannel = null;
shownClient = null;
inChatTab = false;
render();
}
public void setDescriptionHandler(DescriptionHandler handler) {
this.descriptionHandler = handler;
}
/** Runs when the user closes the info chat tab; shows details in the panel again. */
private void onInfoTabClosed() {
inChatTab = false;
render();
}
public void showChannel(ChannelNode ch) {
shownChannel = ch;
shownClient = null;
render();
}
public void showClient(ClientEntry cl, ServerModel model, IconRepository icons) {
shownClient = cl;
shownChannel = null;
shownModel = model;
shownIcons = icons;
render();
}
private void render() {
boolean selected = shownChannel != null || shownClient != null;
boolean hide = inChatTab && selected;
if (descriptionHandler != null) descriptionHandler.setPanelHidden(hide);
if (hide) {
descriptionHandler.showInfoTab(body(), this::onInfoTabClosed);
} else {
setHtml(body() + (selected ? toggleLink(true) : ""));
}
}
private String body() {
if (shownChannel != null) return channelBody(shownChannel);
if (shownClient != null) return clientBody(shownClient, shownModel, shownIcons);
return "<i style='color:#8a8a8a'>Select a channel or client to see details.</i>";
}
private static String channelBody(ChannelNode ch) {
StringBuilder sb = new StringBuilder();
sb.append(heading(esc(ch.name)));
row(sb, "Type", ch.permanent ? "Permanent" : "Temporary");
@@ -67,10 +136,10 @@ public final class InfoPanel extends JScrollPane {
} else {
sb.append("<i style='color:#8a8a8a'>Loading description…</i>");
}
setHtml(sb.toString());
return sb.toString();
}
public void showClient(ClientEntry cl, ServerModel model, IconRepository icons) {
private static String clientBody(ClientEntry cl, ServerModel model, IconRepository icons) {
StringBuilder sb = new StringBuilder();
sb.append(heading(esc(cl.nickname) + (cl.self ? " <span style='color:#8a8a8a'>(you)</span>" : "")));
@@ -105,7 +174,13 @@ public final class InfoPanel extends JScrollPane {
if (cl.description != null && !cl.description.isEmpty()) {
sb.append("<hr><div>").append(multiline(cl.description)).append("</div>");
}
setHtml(sb.toString());
return sb.toString();
}
/** The link that toggles between showing details here and in a chat tab. */
private static String toggleLink(boolean toChat) {
return "<div style='margin-top:8px'><a href='" + TOGGLE_HREF + "' style='font-size:10px'>"
+ (toChat ? "Show in chat tab" : "Show here") + "</a></div>";
}
private void setHtml(String body) {