Tabbed chats with BBCode rendering and clickable identities
Replace the chat target dropdown with a tab strip along the bottom of the chat panel: a Server and a Channel tab plus one closable tab per private chat, each with its own log and an unread marker. Render logs and channel/client descriptions as HTML through a new BBCode renderer in core, supporting b/i/u/s, color, size, url, center and lists, with auto-linked bare URLs. Input is escaped and tag values validated, so server-supplied text cannot inject markup or unsafe schemes. Sender names and TeamSpeak's own client:// links open the same context menu as the server tree (menus extracted into ClientMenu/ChannelMenu); channel:// links open the channel menu. Links are styled by CSS class: identities bold in the author colour, other TS protocols plain, and links leaving the client underlined so they cannot be mistaken for an identity. Dragging a client or channel out of the tree yields the TS3 link BBCode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,18 +3,21 @@ package com.ts3client.ui;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.ServerModel;
|
||||
import com.ts3client.text.TsLink;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTree;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.TransferHandler;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import javax.swing.tree.DefaultTreeModel;
|
||||
import javax.swing.tree.DefaultTreeCellRenderer;
|
||||
import javax.swing.tree.TreePath;
|
||||
import java.awt.Component;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.datatransfer.Transferable;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.List;
|
||||
@@ -67,6 +70,32 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
setViewportView(tree);
|
||||
getViewport().setBackground(Theme.TREE_BG);
|
||||
|
||||
// Dragging a client or channel out of the tree yields the TS3 link BBCode,
|
||||
// which the chat input accepts as plain text.
|
||||
tree.setDragEnabled(true);
|
||||
tree.setTransferHandler(new TransferHandler() {
|
||||
@Override
|
||||
public int getSourceActions(JComponent c) {
|
||||
return COPY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Transferable createTransferable(JComponent c) {
|
||||
TreePath path = tree.getSelectionPath();
|
||||
if (path == null) return null;
|
||||
Object obj = ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
|
||||
if (obj instanceof ClientEntry) {
|
||||
ClientEntry cl = (ClientEntry) obj;
|
||||
return new StringSelection(TsLink.clientBBCode(cl.id, cl.uniqueId, cl.nickname));
|
||||
}
|
||||
if (obj instanceof ChannelNode && !Spacers.isSpacer(((ChannelNode) obj).name)) {
|
||||
ChannelNode ch = (ChannelNode) obj;
|
||||
return new StringSelection(TsLink.channelBBCode(ch.id, ch.name));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
tree.addTreeSelectionListener(e -> {
|
||||
TreePath path = tree.getSelectionPath();
|
||||
Object obj = null;
|
||||
@@ -124,42 +153,12 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
}
|
||||
|
||||
private void showClientMenu(ClientEntry client, MouseEvent e) {
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
if (client.id != selfClientId) {
|
||||
JMenuItem pm = new JMenuItem("Open text chat");
|
||||
pm.addActionListener(a -> actions.openPrivateChat(client));
|
||||
menu.add(pm);
|
||||
JMenuItem poke = new JMenuItem("Poke");
|
||||
poke.addActionListener(a -> actions.pokeClient(client));
|
||||
menu.add(poke);
|
||||
menu.addSeparator();
|
||||
boolean muted = actions.isClientLocallyMuted(client.id);
|
||||
JMenuItem mute = new JMenuItem(muted ? "Unmute client" : "Mute client");
|
||||
mute.addActionListener(a -> actions.toggleClientMute(client));
|
||||
menu.add(mute);
|
||||
} else {
|
||||
JMenuItem self = new JMenuItem("This is you");
|
||||
self.setEnabled(false);
|
||||
menu.add(self);
|
||||
}
|
||||
menu.addSeparator();
|
||||
JMenuItem info = new JMenuItem("Connection Info");
|
||||
info.addActionListener(a -> actions.showConnectionInfo(client));
|
||||
menu.add(info);
|
||||
menu.show(tree, e.getX(), e.getY());
|
||||
ClientMenu.build(client, client.id == selfClientId, actions).show(tree, e.getX(), e.getY());
|
||||
}
|
||||
|
||||
private void showChannelMenu(ChannelNode channel, MouseEvent e) {
|
||||
if (Spacers.isSpacer(channel.name)) return; // spacers aren't interactive
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
JMenuItem join = new JMenuItem("Join channel");
|
||||
join.addActionListener(a -> actions.joinChannel(channel.id));
|
||||
menu.add(join);
|
||||
menu.addSeparator();
|
||||
JMenuItem files = new JMenuItem("Browse files");
|
||||
files.addActionListener(a -> actions.browseFiles(channel));
|
||||
menu.add(files);
|
||||
menu.show(tree, e.getX(), e.getY());
|
||||
ChannelMenu.build(channel, actions).show(tree, e.getX(), e.getY());
|
||||
}
|
||||
|
||||
/** Rebuilds the tree from the model, preserving full expansion. */
|
||||
|
||||
Reference in New Issue
Block a user