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:
@@ -4,6 +4,7 @@ import com.ts3client.text.BBCode;
|
||||
import com.ts3client.text.TsLink;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JEditorPane;
|
||||
import javax.swing.JLabel;
|
||||
@@ -25,6 +26,7 @@ import java.awt.FlowLayout;
|
||||
import java.awt.Point;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseWheelEvent;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
@@ -62,6 +64,8 @@ public final class ChatPanel extends JPanel {
|
||||
private final Tab channelTab = new Tab(Target.CHANNEL, 0, "Channel");
|
||||
/** Private chats keyed by the peer's client id. */
|
||||
private final Map<Integer, Tab> privateTabs = new LinkedHashMap<>();
|
||||
/** Static-content tabs (e.g. a moved-out description), keyed by caller-chosen id. */
|
||||
private final Map<String, Tab> noteTabs = new LinkedHashMap<>();
|
||||
|
||||
private SendHandler sendHandler;
|
||||
private LinkHandler linkHandler;
|
||||
@@ -70,6 +74,7 @@ public final class ChatPanel extends JPanel {
|
||||
super(new BorderLayout());
|
||||
|
||||
tabs.setFont(Theme.UI_FONT);
|
||||
tabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
|
||||
addTab(serverTab, false);
|
||||
addTab(channelTab, false);
|
||||
tabs.setSelectedIndex(0);
|
||||
@@ -77,6 +82,7 @@ public final class ChatPanel extends JPanel {
|
||||
Tab t = selectedTab();
|
||||
if (t != null) t.setUnread(false);
|
||||
});
|
||||
tabs.addMouseWheelListener(this::onWheel);
|
||||
add(tabs, BorderLayout.CENTER);
|
||||
|
||||
JPanel bottom = new JPanel(new BorderLayout(4, 0));
|
||||
@@ -130,6 +136,40 @@ public final class ChatPanel extends JPanel {
|
||||
});
|
||||
}
|
||||
|
||||
/** Drops every static-content tab (moved-out descriptions), e.g. after disconnecting. */
|
||||
public void closeNoteTabs() {
|
||||
edt(() -> {
|
||||
for (Tab t : noteTabs.values()) tabs.remove(t.scroll);
|
||||
noteTabs.clear();
|
||||
});
|
||||
}
|
||||
|
||||
/** Closes a single static-content tab by its key, if open; does not run its {@code onClose}. */
|
||||
public void closeNoteTab(String key) {
|
||||
edt(() -> {
|
||||
Tab tab = noteTabs.remove(key);
|
||||
if (tab != null) tabs.remove(tab.scroll);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens (or replaces the content of, and focuses) a static-content tab, e.g. a
|
||||
* channel/client description moved out of the info panel. {@code onClose} runs
|
||||
* when the tab is closed, so the caller can un-hide the description again.
|
||||
*/
|
||||
public void openDescriptionTab(String key, Icon icon, String title, String htmlBody, Runnable onClose) {
|
||||
edt(() -> {
|
||||
Tab tab = noteTabs.get(key);
|
||||
if (tab == null) {
|
||||
tab = new Tab(key, icon, title, onClose);
|
||||
noteTabs.put(key, tab);
|
||||
addTab(tab, true);
|
||||
}
|
||||
tab.setStaticContent(htmlBody);
|
||||
tabs.setSelectedComponent(tab.scroll);
|
||||
});
|
||||
}
|
||||
|
||||
private void fireSend() {
|
||||
String text = input.getText().trim();
|
||||
if (text.isEmpty() || sendHandler == null) return;
|
||||
@@ -147,6 +187,12 @@ public final class ChatPanel extends JPanel {
|
||||
+ BBCode.escape(text) + "</span>"));
|
||||
}
|
||||
|
||||
/** Server-side events (client joins/leaves/moves, group changes, channel edits, …). */
|
||||
public void appendServerLog(String text) {
|
||||
edt(() -> serverTab.appendLine("<span style=\"color:" + hex(Theme.CHANNEL_TEXT) + "\">" + stamp()
|
||||
+ BBCode.escape(text) + "</span>"));
|
||||
}
|
||||
|
||||
public void appendServerMessage(int fromId, String from, String text) {
|
||||
edt(() -> serverTab.appendMessage(fromId, from, text));
|
||||
}
|
||||
@@ -180,10 +226,25 @@ public final class ChatPanel extends JPanel {
|
||||
}
|
||||
|
||||
private void closeTab(Tab tab) {
|
||||
privateTabs.remove(tab.clientId);
|
||||
if (tab.noteKey != null) {
|
||||
noteTabs.remove(tab.noteKey);
|
||||
if (tab.onClose != null) tab.onClose.run();
|
||||
} else {
|
||||
privateTabs.remove(tab.clientId);
|
||||
}
|
||||
tabs.remove(tab.scroll);
|
||||
}
|
||||
|
||||
private void onWheel(MouseWheelEvent e) {
|
||||
Component content = tabs.getSelectedComponent();
|
||||
if (content != null && content.getBounds().contains(e.getPoint())) return;
|
||||
int steps = e.getWheelRotation();
|
||||
if (steps == 0) return;
|
||||
int target = Math.max(0, Math.min(tabs.getTabCount() - 1, tabs.getSelectedIndex() + steps));
|
||||
if (target != tabs.getSelectedIndex()) tabs.setSelectedIndex(target);
|
||||
e.consume();
|
||||
}
|
||||
|
||||
private Tab selectedTab() {
|
||||
Component c = tabs.getSelectedComponent();
|
||||
if (c == serverTab.scroll) return serverTab;
|
||||
@@ -207,13 +268,29 @@ public final class ChatPanel extends JPanel {
|
||||
final String title;
|
||||
final JEditorPane log = new JEditorPane();
|
||||
final JScrollPane scroll;
|
||||
/** Set for a static-content tab (e.g. a moved-out description); null for a conversation. */
|
||||
final String noteKey;
|
||||
final Runnable onClose;
|
||||
private final Icon icon;
|
||||
private final HTMLDocument doc;
|
||||
private JLabel titleLabel;
|
||||
|
||||
Tab(Target target, int clientId, String title) {
|
||||
this(target, clientId, title, null, null, null);
|
||||
}
|
||||
|
||||
/** A static-content tab: no send target, closing it runs {@code onClose}. */
|
||||
Tab(String noteKey, Icon icon, String title, Runnable onClose) {
|
||||
this(null, 0, title, icon, noteKey, onClose);
|
||||
}
|
||||
|
||||
private Tab(Target target, int clientId, String title, Icon icon, String noteKey, Runnable onClose) {
|
||||
this.target = target;
|
||||
this.clientId = clientId;
|
||||
this.title = title;
|
||||
this.icon = icon;
|
||||
this.noteKey = noteKey;
|
||||
this.onClose = onClose;
|
||||
|
||||
HTMLEditorKit kit = new HTMLEditorKit();
|
||||
StyleSheet css = new StyleSheet();
|
||||
@@ -259,7 +336,7 @@ public final class ChatPanel extends JPanel {
|
||||
Component header(boolean closable) {
|
||||
JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
|
||||
p.setOpaque(false);
|
||||
titleLabel = new JLabel(title);
|
||||
titleLabel = new JLabel(title, icon, JLabel.LEADING);
|
||||
titleLabel.setFont(Theme.UI_FONT);
|
||||
p.add(titleLabel);
|
||||
if (closable) {
|
||||
@@ -303,6 +380,12 @@ public final class ChatPanel extends JPanel {
|
||||
+ sender + ": " + BBCode.toHtml(text));
|
||||
}
|
||||
|
||||
/** Replaces the whole log with fixed content; used by static-content tabs. */
|
||||
void setStaticContent(String html) {
|
||||
log.setText("<html><body><div id=\"chatlog\">" + html + "</div></body></html>");
|
||||
log.setCaretPosition(0);
|
||||
}
|
||||
|
||||
void appendLine(String html) {
|
||||
try {
|
||||
doc.insertBeforeEnd(doc.getElement("chatlog"), "<div>" + html + "</div>");
|
||||
|
||||
Reference in New Issue
Block a user