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:
2026-08-19 18:56:17 +00:00
parent 275de77c8b
commit 9ba0982f25
3 changed files with 108 additions and 2 deletions

View File

@@ -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();