diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java b/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java index 797bf47..4ac9063 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java @@ -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(); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java index 0f87dad..c0d2fcc 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java @@ -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(); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/Theme.java b/ts3-client/swing/src/main/java/com/ts3client/ui/Theme.java index f405428..21234ff 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/Theme.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/Theme.java @@ -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);