Drag and drop clients and channels in the server tree
Clients can be dragged into a channel and channels re-parented or reordered, with an insertion line (or a row outline, for "into this channel") showing where the drop lands. Channel positions come from channel_order, which is the id of the channel above rather than an index, so siblings are now ordered by walking that chain and the neighbour links are repaired locally on create, move and delete -- the server only announces the channel that changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,7 +82,32 @@ public final class ServerModel {
|
||||
}
|
||||
|
||||
public synchronized void removeChannel(int id) {
|
||||
channels.remove(id);
|
||||
ChannelNode c = channels.remove(id);
|
||||
if (c == null) return;
|
||||
// Whatever followed it now follows its predecessor.
|
||||
for (ChannelNode o : channels.values()) {
|
||||
if (o.parentId == c.parentId && o.order == id) o.order = c.order;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Places a channel after {@code newOrder} (0 = first) under {@code newParentId},
|
||||
* repairing the sibling links the server does not re-announce: the channel that
|
||||
* used to follow it, and the one that follows it now.
|
||||
*/
|
||||
public synchronized void relinkChannel(int cid, int newParentId, int newOrder) {
|
||||
ChannelNode c = channels.get(cid);
|
||||
if (c == null) return;
|
||||
for (ChannelNode o : channels.values()) {
|
||||
if (o.id == cid) continue;
|
||||
if (o.parentId == c.parentId && o.order == cid) o.order = c.order;
|
||||
}
|
||||
for (ChannelNode o : channels.values()) {
|
||||
if (o.id == cid) continue;
|
||||
if (o.parentId == newParentId && o.order == newOrder) o.order = cid;
|
||||
}
|
||||
c.parentId = newParentId;
|
||||
c.order = newOrder;
|
||||
}
|
||||
|
||||
/** The "/"-separated path of a channel from the root, e.g. {@code "Lobby/Games"}. */
|
||||
@@ -185,20 +210,45 @@ public final class ServerModel {
|
||||
if (ch != null) ch.clients.add(cl);
|
||||
}
|
||||
|
||||
Comparator<ChannelNode> byOrder = Comparator.comparingInt((ChannelNode c) -> c.order)
|
||||
.thenComparing(c -> c.name == null ? "" : c.name.toLowerCase());
|
||||
Comparator<ClientEntry> byClient = Comparator
|
||||
.comparingInt((ClientEntry c) -> -c.talkPower)
|
||||
.thenComparing(c -> c.nickname == null ? "" : c.nickname.toLowerCase());
|
||||
|
||||
roots.sort(byOrder);
|
||||
sortSiblings(roots);
|
||||
for (ChannelNode c : channels.values()) {
|
||||
c.children.sort(byOrder);
|
||||
sortSiblings(c.children);
|
||||
c.clients.sort(byClient);
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders one set of sibling channels in place. TS3 stores the position as
|
||||
* {@code channel_order} = the id of the channel above it (0 = topmost), so the
|
||||
* siblings form a linked list rather than a sortable key.
|
||||
*/
|
||||
private static void sortSiblings(List<ChannelNode> siblings) {
|
||||
if (siblings.size() < 2) return;
|
||||
Map<Integer, ChannelNode> byPredecessor = new LinkedHashMap<>();
|
||||
for (ChannelNode c : siblings) byPredecessor.putIfAbsent(c.order, c);
|
||||
|
||||
List<ChannelNode> ordered = new ArrayList<>(siblings.size());
|
||||
java.util.Set<Integer> placed = new java.util.HashSet<>();
|
||||
for (int key = 0; ; ) {
|
||||
ChannelNode next = byPredecessor.get(key);
|
||||
if (next == null || !placed.add(next.id)) break;
|
||||
ordered.add(next);
|
||||
key = next.id;
|
||||
}
|
||||
// A broken chain (duplicate or dangling links) leaves stragglers; keep them
|
||||
// rather than dropping channels from the tree.
|
||||
for (ChannelNode c : siblings) {
|
||||
if (!placed.contains(c.id)) ordered.add(c);
|
||||
}
|
||||
siblings.clear();
|
||||
siblings.addAll(ordered);
|
||||
}
|
||||
|
||||
public synchronized int clientCount() {
|
||||
int n = 0;
|
||||
for (ClientEntry c : clients.values()) if (!c.isQuery()) n++;
|
||||
|
||||
@@ -428,6 +428,38 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
}, "ts3j-join").start();
|
||||
}
|
||||
|
||||
/** Moves another client (or ourselves) into a channel. */
|
||||
public void moveClient(int clientId, int channelId, String password) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
client.clientMove(clientId, channelId,
|
||||
(password == null || password.isEmpty()) ? null : password);
|
||||
} catch (Exception e) {
|
||||
ui.onError("Could not move client: " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-move-client").start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-parents and repositions a channel.
|
||||
*
|
||||
* @param orderPredecessorId the channel this one should sit below among its new
|
||||
* siblings, or 0 to place it first
|
||||
*/
|
||||
public void moveChannel(int channelId, int newParentId, int orderPredecessorId) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
SingleCommand cmd = new SingleCommand("channelmove", ProtocolRole.CLIENT);
|
||||
cmd.add(new CommandSingleParameter("cid", Integer.toString(channelId)));
|
||||
cmd.add(new CommandSingleParameter("cpid", Integer.toString(newParentId)));
|
||||
cmd.add(new CommandSingleParameter("order", Integer.toString(orderPredecessorId)));
|
||||
client.executeCommand(cmd).complete();
|
||||
} catch (Exception e) {
|
||||
ui.onError("Could not move channel: " + rootMessage(e));
|
||||
}
|
||||
}, "ts3j-move-channel").start();
|
||||
}
|
||||
|
||||
public void sendChannelMessage(String text) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
@@ -570,6 +602,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
if (pid == 0) pid = safeInt(e, "pid");
|
||||
int order = safeInt(e, "channel_order");
|
||||
model.putChannel(cid, name, pid, order);
|
||||
model.relinkChannel(cid, pid, order);
|
||||
ui.onModelChanged();
|
||||
}
|
||||
|
||||
@@ -594,8 +627,9 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
public void onChannelMoved(ChannelMovedEvent e) {
|
||||
ChannelNode ch = model.getChannel(safeInt(e, "cid"));
|
||||
if (ch != null) {
|
||||
if (e.get("cpid") != null) ch.parentId = e.getInt("cpid");
|
||||
if (e.get("order") != null) ch.order = e.getInt("order");
|
||||
int parent = e.get("cpid") != null ? e.getInt("cpid") : ch.parentId;
|
||||
int order = e.get("order") != null ? e.getInt("order") : ch.order;
|
||||
model.relinkChannel(ch.id, parent, order);
|
||||
ui.onModelChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,6 +292,21 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions {
|
||||
if (conn.isConnected()) conn.joinChannel(channelId, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveClientToChannel(ClientEntry client, ChannelNode target) {
|
||||
if (!conn.isConnected()) return;
|
||||
if (client.id == conn.getSelfClientId()) {
|
||||
conn.joinChannel(target.id, null);
|
||||
} else {
|
||||
conn.moveClient(client.id, target.id, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveChannel(ChannelNode channel, int newParentId, int orderPredecessorId) {
|
||||
if (conn.isConnected()) conn.moveChannel(channel.id, newParentId, orderPredecessorId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openPrivateChat(ClientEntry client) {
|
||||
chatPanel.openPrivateChat(client.id, client.nickname);
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.ServerModel;
|
||||
import com.ts3client.text.TsLink;
|
||||
|
||||
import javax.swing.DropMode;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JScrollPane;
|
||||
@@ -14,10 +15,17 @@ import javax.swing.TransferHandler;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import javax.swing.tree.DefaultTreeModel;
|
||||
import javax.swing.tree.DefaultTreeCellRenderer;
|
||||
import javax.swing.tree.TreeModel;
|
||||
import javax.swing.tree.TreePath;
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Component;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.Transferable;
|
||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.List;
|
||||
@@ -47,9 +55,25 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
|
||||
/** A channel or client node was selected (or {@code null} when cleared). */
|
||||
void onSelectionChanged(Object userObject);
|
||||
|
||||
/** Drag-and-drop of a client onto a channel. */
|
||||
void moveClientToChannel(ClientEntry client, ChannelNode target);
|
||||
|
||||
/**
|
||||
* Drag-and-drop of a channel to a new place in the tree.
|
||||
*
|
||||
* @param orderPredecessorId the channel it should sit below among its new
|
||||
* siblings, or 0 to place it first
|
||||
*/
|
||||
void moveChannel(ChannelNode channel, int newParentId, int orderPredecessorId);
|
||||
}
|
||||
|
||||
private final JTree tree;
|
||||
/** 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 DropIndicatorTree tree;
|
||||
private final DefaultMutableTreeNode root = new DefaultMutableTreeNode();
|
||||
private final DefaultTreeModel treeModel = new DefaultTreeModel(root);
|
||||
private final ServerModel model;
|
||||
@@ -60,7 +84,7 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
this.model = model;
|
||||
this.actions = actions;
|
||||
root.setUserObject("Not connected");
|
||||
this.tree = new JTree(treeModel);
|
||||
this.tree = new DropIndicatorTree(treeModel);
|
||||
tree.setRootVisible(true);
|
||||
tree.setShowsRootHandles(true);
|
||||
tree.setRowHeight(20);
|
||||
@@ -70,31 +94,11 @@ 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.
|
||||
// Within the tree a drag moves the client or channel; dropped elsewhere it
|
||||
// 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.setDropMode(DropMode.ON_OR_INSERT);
|
||||
tree.setTransferHandler(new TreeTransferHandler());
|
||||
|
||||
tree.addTreeSelectionListener(e -> {
|
||||
TreePath path = tree.getSelectionPath();
|
||||
@@ -161,6 +165,281 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
ChannelMenu.build(channel, actions).show(tree, e.getX(), e.getY());
|
||||
}
|
||||
|
||||
// ---- drag and drop ----
|
||||
|
||||
private static DefaultMutableTreeNode nodeOf(TreePath path) {
|
||||
return path == null ? null : (DefaultMutableTreeNode) path.getLastPathComponent();
|
||||
}
|
||||
|
||||
/** The path of the tree node showing {@code target}, or {@code null}. */
|
||||
private TreePath pathOf(Object target) {
|
||||
java.util.Enumeration<?> nodes = root.breadthFirstEnumeration();
|
||||
while (nodes.hasMoreElements()) {
|
||||
DefaultMutableTreeNode n = (DefaultMutableTreeNode) nodes.nextElement();
|
||||
if (n.getUserObject() == target) return new TreePath(n.getPath());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 != root) {
|
||||
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((ChannelNode) 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws where the drop will land: an insertion line between rows, or an outline
|
||||
* around the row that will receive the dragged node.
|
||||
*/
|
||||
private static final class DropIndicatorTree extends JTree {
|
||||
/** Set while a drop would move a client into this channel row. */
|
||||
private TreePath highlight;
|
||||
|
||||
DropIndicatorTree(TreeModel model) {
|
||||
super(model);
|
||||
}
|
||||
|
||||
void highlightChannel(TreePath path) {
|
||||
if (path == highlight || (path != null && path.equals(highlight))) return;
|
||||
highlight = path;
|
||||
repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
JTree.DropLocation loc = getDropLocation();
|
||||
if (loc == null || loc.getPath() == null) return;
|
||||
|
||||
Graphics2D g2 = (Graphics2D) g.create();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setColor(Theme.ACCENT);
|
||||
if (highlight != null || loc.getChildIndex() < 0) {
|
||||
Rectangle r = getPathBounds(highlight != null ? highlight : loc.getPath());
|
||||
if (r != null) {
|
||||
g2.setStroke(new BasicStroke(2f));
|
||||
g2.drawRoundRect(r.x, r.y + 1, r.width - 1, r.height - 3, 4, 4);
|
||||
}
|
||||
} else {
|
||||
Rectangle line = insertLine(loc);
|
||||
if (line != null) {
|
||||
g2.fillRect(line.x, line.y - 1, line.width, 2);
|
||||
g2.fillOval(line.x - 3, line.y - 4, 7, 7);
|
||||
}
|
||||
}
|
||||
g2.dispose();
|
||||
}
|
||||
|
||||
/** The 1px-tall strip where the insertion line goes, in tree coordinates. */
|
||||
private Rectangle insertLine(JTree.DropLocation loc) {
|
||||
DefaultMutableTreeNode parent = (DefaultMutableTreeNode) loc.getPath().getLastPathComponent();
|
||||
int index = loc.getChildIndex();
|
||||
if (index < parent.getChildCount()) {
|
||||
Rectangle r = getPathBounds(loc.getPath().pathByAddingChild(parent.getChildAt(index)));
|
||||
return r == null ? null : new Rectangle(r.x, r.y, getWidth() - r.x, 2);
|
||||
}
|
||||
if (parent.getChildCount() == 0) {
|
||||
Rectangle r = getPathBounds(loc.getPath());
|
||||
if (r == null) return null;
|
||||
int x = r.x + getRowHeight();
|
||||
return new Rectangle(x, r.y + r.height, getWidth() - x, 2);
|
||||
}
|
||||
// Past the last child: below that child's whole (expanded) subtree.
|
||||
TreePath lastChild = loc.getPath().pathByAddingChild(parent.getChildAt(parent.getChildCount() - 1));
|
||||
Rectangle head = getPathBounds(lastChild);
|
||||
Rectangle tail = getPathBounds(lastVisibleRow(lastChild));
|
||||
if (head == null || tail == null) return null;
|
||||
return new Rectangle(head.x, tail.y + tail.height, getWidth() - head.x, 2);
|
||||
}
|
||||
|
||||
private TreePath lastVisibleRow(TreePath path) {
|
||||
int row = getRowForPath(path);
|
||||
if (row < 0) return path;
|
||||
for (int i = row + 1; i < getRowCount(); i++) {
|
||||
if (!path.isDescendant(getPathForRow(i))) break;
|
||||
row = i;
|
||||
}
|
||||
return getPathForRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
/** Rebuilds the tree from the model, preserving full expansion. */
|
||||
public void rebuild() {
|
||||
root.setUserObject(model.getServerName());
|
||||
|
||||
Reference in New Issue
Block a user