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 pathOf; ServerTreeDragAndDrop(ServerModel model, ServerTreePanel.Actions actions, DropIndicatorTree tree, Function 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); } } }