Extract cell rendering, right-aligned group-icon-strip painting, the drop-indicator JTree subclass, and drag-and-drop handling out of ServerTreePanel into ServerTreeCellRenderer, DropIndicatorTree, and ServerTreeDragAndDrop, leaving ServerTreePanel as the coordinator (706 -> 284 lines). Pure refactor, no behavior change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
236 lines
9.6 KiB
Java
236 lines
9.6 KiB
Java
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.JComponent;
|
|
import javax.swing.JTree;
|
|
import javax.swing.TransferHandler;
|
|
import javax.swing.tree.DefaultMutableTreeNode;
|
|
import javax.swing.tree.TreePath;
|
|
import java.awt.datatransfer.DataFlavor;
|
|
import java.awt.datatransfer.Transferable;
|
|
import java.awt.datatransfer.UnsupportedFlavorException;
|
|
import java.util.function.Function;
|
|
|
|
/**
|
|
* Drag-and-drop for the {@link ServerTreePanel} tree: dragging a client onto a
|
|
* channel moves it there, dragging a channel reorders/reparents it, and dropping
|
|
* either outside the tree yields its TS3 link BBCode, which the chat input accepts
|
|
* as plain text.
|
|
*/
|
|
final class ServerTreeDragAndDrop {
|
|
|
|
/** Carries the dragged node inside this JVM; drops elsewhere get the BBCode text. */
|
|
private static final DataFlavor NODE_FLAVOR = new DataFlavor(
|
|
DataFlavor.javaJVMLocalObjectMimeType + ";class=java.lang.Object",
|
|
"TeamSpeak tree node");
|
|
|
|
private final ServerModel model;
|
|
private final ServerTreePanel.Actions actions;
|
|
private final DropIndicatorTree tree;
|
|
/** Looks up the tree path currently showing a given channel/client, or {@code null}. */
|
|
private final Function<Object, TreePath> pathOf;
|
|
|
|
ServerTreeDragAndDrop(ServerModel model, ServerTreePanel.Actions actions, DropIndicatorTree tree,
|
|
Function<Object, TreePath> pathOf) {
|
|
this.model = model;
|
|
this.actions = actions;
|
|
this.tree = tree;
|
|
this.pathOf = pathOf;
|
|
}
|
|
|
|
TransferHandler transferHandler() {
|
|
return new TreeTransferHandler();
|
|
}
|
|
|
|
private static DefaultMutableTreeNode nodeOf(TreePath path) {
|
|
return path == null ? null : (DefaultMutableTreeNode) path.getLastPathComponent();
|
|
}
|
|
|
|
/**
|
|
* The channel a dragged client would land in, or {@code null} if the drop makes
|
|
* no sense (outside a channel, or the channel the client is already in).
|
|
*/
|
|
private ChannelNode resolveClientDrop(JTree.DropLocation loc, ClientEntry dragged) {
|
|
DefaultMutableTreeNode target = nodeOf(loc.getPath());
|
|
if (target == null) return null;
|
|
Object obj = target.getUserObject();
|
|
ChannelNode channel = null;
|
|
if (obj instanceof ChannelNode) {
|
|
channel = (ChannelNode) obj;
|
|
} else if (obj instanceof ClientEntry) {
|
|
channel = model.getChannel(((ClientEntry) obj).channelId);
|
|
}
|
|
if (channel == null || Spacers.isSpacer(channel.name)) return null;
|
|
if (channel.id == dragged.channelId) return null;
|
|
return channel;
|
|
}
|
|
|
|
/**
|
|
* The new parent and predecessor for a dragged channel as {@code {cpid, order}},
|
|
* or {@code null} if this drop is not a legal (or meaningful) move.
|
|
*/
|
|
private int[] resolveChannelDrop(JTree.DropLocation loc, ChannelNode dragged) {
|
|
DefaultMutableTreeNode target = nodeOf(loc.getPath());
|
|
if (target == null) return null;
|
|
|
|
DefaultMutableTreeNode parentNode;
|
|
int insertIndex;
|
|
if (loc.getChildIndex() >= 0) {
|
|
parentNode = target;
|
|
insertIndex = loc.getChildIndex();
|
|
} else {
|
|
// Dropped onto a node: become its last subchannel.
|
|
parentNode = target.getUserObject() instanceof ClientEntry
|
|
? (DefaultMutableTreeNode) target.getParent() : target;
|
|
if (parentNode == null) return null;
|
|
insertIndex = parentNode.getChildCount();
|
|
}
|
|
|
|
int parentId = 0;
|
|
Object parentObj = parentNode.getUserObject();
|
|
if (parentObj instanceof ChannelNode) {
|
|
ChannelNode parent = (ChannelNode) parentObj;
|
|
if (Spacers.isSpacer(parent.name)) return null;
|
|
if (isSelfOrDescendant(parent, dragged)) return null; // would detach the subtree
|
|
parentId = parent.id;
|
|
} else if (parentNode.getParent() != null) {
|
|
return null;
|
|
}
|
|
|
|
int predecessorId = 0;
|
|
for (int i = 0; i < insertIndex && i < parentNode.getChildCount(); i++) {
|
|
Object o = ((DefaultMutableTreeNode) parentNode.getChildAt(i)).getUserObject();
|
|
if (o instanceof ChannelNode && ((ChannelNode) o).id != dragged.id) {
|
|
predecessorId = ((ChannelNode) o).id;
|
|
}
|
|
}
|
|
if (parentId == dragged.parentId && predecessorId == dragged.order) return null; // no-op
|
|
return new int[]{parentId, predecessorId};
|
|
}
|
|
|
|
/** Whether {@code candidate} is {@code ancestor} itself or sits below it. */
|
|
private boolean isSelfOrDescendant(ChannelNode candidate, ChannelNode ancestor) {
|
|
ChannelNode c = candidate;
|
|
for (int guard = 0; c != null && guard < 64; guard++) {
|
|
if (c.id == ancestor.id) return true;
|
|
c = model.getChannel(c.parentId);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private final class TreeTransferHandler extends TransferHandler {
|
|
@Override
|
|
public int getSourceActions(JComponent c) {
|
|
return COPY | MOVE;
|
|
}
|
|
|
|
@Override
|
|
protected Transferable createTransferable(JComponent c) {
|
|
TreePath path = tree.getSelectionPath();
|
|
if (path == null) return null;
|
|
Object obj = nodeOf(path).getUserObject();
|
|
if (obj instanceof ClientEntry) {
|
|
ClientEntry cl = (ClientEntry) obj;
|
|
return new NodeTransferable(cl, TsLink.clientBBCode(cl.id, cl.uniqueId, cl.nickname));
|
|
}
|
|
if (obj instanceof ChannelNode) {
|
|
ChannelNode ch = (ChannelNode) obj;
|
|
// Spacers have no meaningful link text, but can still be re-ordered.
|
|
return new NodeTransferable(ch, Spacers.isSpacer(ch.name)
|
|
? null : TsLink.channelBBCode(ch.id, ch.name));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public boolean canImport(TransferSupport support) {
|
|
if (!support.isDrop() || !support.isDataFlavorSupported(NODE_FLAVOR)) {
|
|
tree.highlightChannel(null);
|
|
return false;
|
|
}
|
|
if ((support.getSourceDropActions() & MOVE) == MOVE) support.setDropAction(MOVE);
|
|
Object resolved = resolve(support);
|
|
// A client always lands *inside* a channel, so mark that channel instead of
|
|
// drawing a line that would suggest a position among its clients.
|
|
tree.highlightChannel(resolved instanceof ChannelNode
|
|
? pathOf.apply(resolved) : null);
|
|
// The indicator spans the full width, past the row rectangles Swing
|
|
// repaints on its own when the drop location moves.
|
|
tree.repaint();
|
|
return resolved != null;
|
|
}
|
|
|
|
@Override
|
|
protected void exportDone(JComponent source, Transferable data, int action) {
|
|
tree.highlightChannel(null);
|
|
tree.repaint();
|
|
}
|
|
|
|
@Override
|
|
public boolean importData(TransferSupport support) {
|
|
if (!canImport(support)) return false;
|
|
Object dragged = draggedNode(support);
|
|
Object resolved = resolve(support);
|
|
tree.highlightChannel(null);
|
|
if (dragged instanceof ClientEntry) {
|
|
actions.moveClientToChannel((ClientEntry) dragged, (ChannelNode) resolved);
|
|
} else if (dragged instanceof ChannelNode) {
|
|
int[] place = (int[]) resolved;
|
|
actions.moveChannel((ChannelNode) dragged, place[0], place[1]);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** The destination for this drop: a ChannelNode, an {@code {cpid, order}} pair, or null. */
|
|
private Object resolve(TransferSupport support) {
|
|
Object dragged = draggedNode(support);
|
|
if (!(support.getDropLocation() instanceof JTree.DropLocation)) return null;
|
|
JTree.DropLocation loc = (JTree.DropLocation) support.getDropLocation();
|
|
if (dragged instanceof ClientEntry) return resolveClientDrop(loc, (ClientEntry) dragged);
|
|
if (dragged instanceof ChannelNode) return resolveChannelDrop(loc, (ChannelNode) dragged);
|
|
return null;
|
|
}
|
|
|
|
private Object draggedNode(TransferSupport support) {
|
|
try {
|
|
return support.getTransferable().getTransferData(NODE_FLAVOR);
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Offers the dragged node locally and its TS3 link BBCode to other applications. */
|
|
private static final class NodeTransferable implements Transferable {
|
|
private final Object node;
|
|
private final String text;
|
|
|
|
NodeTransferable(Object node, String text) {
|
|
this.node = node;
|
|
this.text = text;
|
|
}
|
|
|
|
@Override
|
|
public DataFlavor[] getTransferDataFlavors() {
|
|
return text == null ? new DataFlavor[]{NODE_FLAVOR}
|
|
: new DataFlavor[]{NODE_FLAVOR, DataFlavor.stringFlavor};
|
|
}
|
|
|
|
@Override
|
|
public boolean isDataFlavorSupported(DataFlavor flavor) {
|
|
return NODE_FLAVOR.equals(flavor) || (text != null && DataFlavor.stringFlavor.equals(flavor));
|
|
}
|
|
|
|
@Override
|
|
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
|
|
if (NODE_FLAVOR.equals(flavor)) return node;
|
|
if (text != null && DataFlavor.stringFlavor.equals(flavor)) return text;
|
|
throw new UnsupportedFlavorException(flavor);
|
|
}
|
|
}
|
|
}
|