Work tree rows by their whole line, and outline the hovered one
Swing hit-tests a tree row only as far as its label, so a click past the end of the name did nothing. Rows are now found by their vertical band alone, which covers selection, the context menus, double-click join and middle-click info; the expand handle to the left of a label still works the row without selecting it. The hovered row is outlined across that same full width, so what a click would act on is visible before making it. Spacers, which nothing can be done with, are skipped, and the outline gives way to the drag-and-drop indicator while a drag is in progress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import com.ts3client.net.ServerModel;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.JTree;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import javax.swing.tree.TreeModel;
|
||||
import javax.swing.tree.TreePath;
|
||||
@@ -14,6 +15,9 @@ import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseWheelEvent;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -34,11 +38,69 @@ final class DropIndicatorTree extends JTree {
|
||||
private final GroupIcons groupIcons;
|
||||
/** Set while a drop would move a client into this channel row. */
|
||||
private TreePath highlight;
|
||||
/** The row the pointer is over, or -1 when it is over none. */
|
||||
private int hoverRow = -1;
|
||||
|
||||
DropIndicatorTree(TreeModel treeModel, ServerModel model, GroupIcons groupIcons) {
|
||||
super(treeModel);
|
||||
this.model = model;
|
||||
this.groupIcons = groupIcons;
|
||||
MouseAdapter hover = new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent e) {
|
||||
setHoverRow(rowAt(e.getY()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
setHoverRow(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
setHoverRow(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseWheelMoved(MouseWheelEvent e) {
|
||||
// The pointer stays put while the rows move under it, so the row it is
|
||||
// over has to be looked up again once the scroll has been applied.
|
||||
SwingUtilities.invokeLater(() -> setHoverRow(rowAt(e.getY())));
|
||||
}
|
||||
};
|
||||
addMouseMotionListener(hover);
|
||||
addMouseListener(hover);
|
||||
addMouseWheelListener(hover);
|
||||
}
|
||||
|
||||
/**
|
||||
* The row a point falls on, going by its vertical band alone: a row reaches
|
||||
* across the whole width, not just as far as its label.
|
||||
*/
|
||||
int rowAt(int y) {
|
||||
int row = getClosestRowForLocation(0, y);
|
||||
if (row < 0) return -1;
|
||||
Rectangle bounds = getRowBounds(row);
|
||||
return bounds != null && y >= bounds.y && y < bounds.y + bounds.height ? row : -1;
|
||||
}
|
||||
|
||||
/** The path a point falls on, by the same rule as {@link #rowAt(int)}. */
|
||||
TreePath pathAt(int y) {
|
||||
int row = rowAt(y);
|
||||
return row < 0 ? null : getPathForRow(row);
|
||||
}
|
||||
|
||||
private void setHoverRow(int row) {
|
||||
if (row == hoverRow) return;
|
||||
repaintRow(hoverRow);
|
||||
hoverRow = row;
|
||||
repaintRow(row);
|
||||
}
|
||||
|
||||
private void repaintRow(int row) {
|
||||
if (row < 0) return;
|
||||
Rectangle bounds = getRowBounds(row);
|
||||
if (bounds != null) repaint(0, bounds.y, getWidth(), bounds.height);
|
||||
}
|
||||
|
||||
/** Keeps the server row permanently open; collapsing it would hide everything. */
|
||||
@@ -69,6 +131,7 @@ final class DropIndicatorTree extends JTree {
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
paintBadges(g);
|
||||
paintHover(g);
|
||||
JTree.DropLocation loc = getDropLocation();
|
||||
if (loc == null || loc.getPath() == null) return;
|
||||
|
||||
@@ -91,6 +154,30 @@ final class DropIndicatorTree extends JTree {
|
||||
g2.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Outlines the row under the pointer, across the full width the row occupies —
|
||||
* the same area a click on it acts on. Spacers, which nothing can be done with,
|
||||
* are left alone.
|
||||
*/
|
||||
private void paintHover(Graphics g) {
|
||||
if (hoverRow < 0 || getDropLocation() != null) return;
|
||||
Rectangle bounds = getRowBounds(hoverRow);
|
||||
TreePath path = getPathForRow(hoverRow);
|
||||
if (bounds == null || path == null || isSpacer(path)) return;
|
||||
|
||||
Rectangle visible = getVisibleRect();
|
||||
Graphics2D g2 = (Graphics2D) g.create();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setColor(Theme.TREE_HOVER);
|
||||
g2.drawRoundRect(visible.x, bounds.y, visible.width - 1, bounds.height - 1, 4, 4);
|
||||
g2.dispose();
|
||||
}
|
||||
|
||||
private boolean isSpacer(TreePath path) {
|
||||
Object obj = ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
|
||||
return obj instanceof ChannelNode && Spacers.isSpacer(((ChannelNode) obj).name);
|
||||
}
|
||||
|
||||
/** The 1px-tall strip where the insertion line goes, in tree coordinates. */
|
||||
private Rectangle insertLine(JTree.DropLocation loc) {
|
||||
DefaultMutableTreeNode parent = (DefaultMutableTreeNode) loc.getPath().getLastPathComponent();
|
||||
|
||||
@@ -14,6 +14,7 @@ import javax.swing.plaf.basic.BasicTreeUI;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import javax.swing.tree.DefaultTreeModel;
|
||||
import javax.swing.tree.TreePath;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.List;
|
||||
@@ -159,6 +160,7 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
tree.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
selectRowUnder(e);
|
||||
maybePopup(e);
|
||||
}
|
||||
|
||||
@@ -183,6 +185,21 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the row a press landed on. A row acts on its whole line, but Swing's own
|
||||
* hit testing stops at the end of the label, so a press further right would leave
|
||||
* the selection where it was.
|
||||
*/
|
||||
private void selectRowUnder(MouseEvent e) {
|
||||
if (!SwingUtilities.isLeftMouseButton(e) && !e.isPopupTrigger()) return;
|
||||
TreePath path = tree.pathAt(e.getY());
|
||||
if (path == null || path.equals(tree.getSelectionPath())) return;
|
||||
Rectangle bounds = tree.getPathBounds(path);
|
||||
// Left of the label is the expand handle, which Swing works the row without selecting it.
|
||||
if (bounds != null && e.getX() < bounds.x) return;
|
||||
tree.setSelectionPath(path);
|
||||
}
|
||||
|
||||
public void setSelfClientId(int id) {
|
||||
this.selfClientId = id;
|
||||
}
|
||||
@@ -198,7 +215,7 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
}
|
||||
|
||||
private Object nodeAt(MouseEvent e) {
|
||||
TreePath path = tree.getPathForLocation(e.getX(), e.getY());
|
||||
TreePath path = tree.pathAt(e.getY());
|
||||
if (path == null) return null;
|
||||
DefaultMutableTreeNode n = (DefaultMutableTreeNode) path.getLastPathComponent();
|
||||
return n.getUserObject();
|
||||
@@ -206,7 +223,7 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
|
||||
private void maybePopup(MouseEvent e) {
|
||||
if (!e.isPopupTrigger()) return;
|
||||
TreePath path = tree.getPathForLocation(e.getX(), e.getY());
|
||||
TreePath path = tree.pathAt(e.getY());
|
||||
if (path == null) return;
|
||||
tree.setSelectionPath(path);
|
||||
Object obj = ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
|
||||
|
||||
@@ -9,6 +9,8 @@ public final class Theme {
|
||||
public static final Color WINDOW_BG = new Color(0xF0F0F0);
|
||||
public static final Color TREE_BG = new Color(0xFFFFFF);
|
||||
public static final Color TREE_SELECTION = new Color(0xCFE3FB);
|
||||
/** Outline drawn around the row the pointer is over. */
|
||||
public static final Color TREE_HOVER = new Color(0x9CC4EE);
|
||||
public static final Color TREE_TEXT = new Color(0x1E1E1E);
|
||||
public static final Color CHANNEL_TEXT = new Color(0x21486B);
|
||||
public static final Color SERVER_TEXT = new Color(0x123456);
|
||||
|
||||
Reference in New Issue
Block a user