package org.simantics.district.network.ui.nodes; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.function.ToIntFunction; import org.simantics.district.network.ui.styles.DistrictNetworkHoverInfoStyle; import org.simantics.scenegraph.INode; import org.simantics.scenegraph.NodeException; import org.simantics.scenegraph.ParentNode; import org.simantics.scenegraph.g2d.G2DNode; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.g2d.nodes.spatial.RTreeNode; import org.simantics.scenegraph.profile.common.ProfileVariables; import org.simantics.scenegraph.utils.DPIUtil; import org.simantics.scenegraph.utils.GeometryUtils; import org.simantics.scenegraph.utils.NodeUtil; import org.simantics.scl.runtime.Lists; import org.simantics.scl.runtime.tuple.Tuple3; public class DistrictNetworkHoverInfoNode extends G2DNode implements HoverSensitiveNode, DeferredNode { private static final long serialVersionUID = 1L; public static final String NODE_KEY = "DISTRICT_NETWORK_HOVER_INFO"; private static final int PAD = 15; private List labels; private Font font = new Font(Font.SANS_SERIF, Font.PLAIN, DPIUtil.upscale(14)); @SuppressWarnings("unused") private Point2D origin; private boolean hover = false; private Point2D mousePosition; /** * For rendering and clipping the hover info base rectangle. */ private Rectangle2D bgRect = new Rectangle2D.Double(); private static AtomicReference activeNode = new AtomicReference<>(); @Override public void render(Graphics2D g) { ParentNode root = (ParentNode) NodeUtil.getNearestParentOfType(this, RTreeNode.class); DeferredRenderingNode deferred = root != null ? (DeferredRenderingNode) root.getNode(DistrictNetworkHoverInfoStyle.HOVER_INFO_DEFERRED) : null; if (deferred != null) deferred.deferNode(g.getTransform(), this); else renderDeferred(g); } @Override public void renderDeferred(Graphics2D g) { if (!hover || activeNode.get() == null) return; if (labels == null || labels.isEmpty() || mousePosition == null) return; AffineTransform ot = g.getTransform(); Font of = g.getFont(); doRender(g); g.setFont(of); g.setTransform(ot); } private ToIntFunction widthMeasurer(FontMetrics fm) { return s -> fm.stringWidth(s); } private void doRender(Graphics2D g) { AffineTransform tt = getTransform(); g.transform(tt); g.translate(mousePosition.getX(), mousePosition.getY()); //g.translate(origin.getX(), origin.getY()); double scale = 1.0 / GeometryUtils.getScale(g.getTransform()); g.scale(scale, scale); g.setFont(font); FontMetrics fm = g.getFontMetrics(); ToIntFunction sizer = widthMeasurer(fm); double rowHeight = fm.getHeight() * 1.1; // Let's calculate the max widths of each column. // However, the last label is assumed to be a title row. List values = labels.subList(0, labels.size() - 1); Tuple3 title = labels.get(labels.size() - 1); int maxLabelWidth = values.stream().map(t -> (String) t.c0).mapToInt(sizer).max().orElse(PAD); int[] valueWidths = values.stream().map(t -> (String) t.c1).mapToInt(sizer).toArray(); int maxValueWidth = Arrays.stream(valueWidths).max().orElse(PAD); int maxUnitWidth = values.stream().map(t -> (String) t.c2).mapToInt(sizer).max().orElse(PAD); String titleString = (Objects.toString(title.c0, "") + " " + Objects.toString(title.c1, "") + " " + Objects.toString(title.c2, "")) .trim(); int titleWidth = sizer.applyAsInt(titleString); int titleRowWidth = PAD + titleWidth + PAD; int maxValueRowWidth = PAD + maxLabelWidth + PAD + maxValueWidth + PAD + maxUnitWidth + PAD; int totalWidth = Math.max(maxValueRowWidth, titleRowWidth); int totalHeight = (int) Math.round((rowHeight) * labels.size()); double minX = -(PAD + maxLabelWidth + PAD + maxValueWidth + PAD); double minY = -(totalHeight + (int)Math.round(rowHeight)); bgRect.setRect(minX, minY, totalWidth, totalHeight + PAD); confineIn(bgRect, g.getClip().getBounds2D()); g.setColor(Color.WHITE); g.fill(bgRect); g.setColor(Color.DARK_GRAY); g.draw(bgRect); g.setColor(Color.BLACK); g.translate(bgRect.getMinX() - minX, bgRect.getMinY() - minY); int rows = values.size(); double y = -rowHeight; float labelX = -(maxLabelWidth + PAD + maxValueWidth + PAD); float unitX = 0; for (int i = 0; i < rows; ++i) { Tuple3 t = values.get(i); float ty = (float) y; if (t.c0 != null && ((String) t.c0).length() > 0) { g.drawString((String) t.c0, labelX, ty); } if (t.c1 != null && ((String) t.c1).length() > 0) { g.drawString((String) t.c1, -(valueWidths[i] + PAD), ty); } if (t.c2 != null && ((String) t.c2).length() > 0) { g.drawString((String) t.c2, unitX, ty); } y -= rowHeight; } if (!titleString.trim().isEmpty()) { int titleX = (int) (labelX - PAD + (bgRect.getWidth() - titleWidth) * 0.5); g.drawString(titleString, titleX, (float) y); } } private Rectangle2D confineIn(Rectangle2D r, Rectangle2D bounds) { // Handle right/bottom overrun double maxX = Math.min(r.getMaxX(), bounds.getMaxX()); double maxY = Math.min(r.getMaxY(), bounds.getMaxY()); r.setFrame( maxX - r.getWidth(), maxY - r.getHeight(), r.getWidth(), r.getHeight()); // Handle left/top overrun r.setFrame( Math.max(r.getMinX(), bounds.getMinX()), Math.max(r.getMinY(), bounds.getMinY()), r.getWidth(), r.getHeight()); return r; } @Override public Rectangle2D getBoundsInLocal() { return null; } @SuppressWarnings("unchecked") public void setLabels(List list) { this.labels = Lists.reverse(list); } public void setOrigin(Point2D origin) { this.origin = origin; } @Override public boolean hover(boolean hover, boolean isConnectionTool) { // hover = hover && activeNode.updateAndGet(current -> current == null ? this : current) == this; boolean changed = hover != this.hover; this.hover = hover; // if (changed) { // if (!hover) activeNode.updateAndGet(current -> current == this ? null : current); // repaint(); // } return changed; } @Override public void setMousePosition(Point2D p) { mousePosition = p; } @Override public void delete() { super.delete(); activeNode.set(null); } public void hover2(G2DParentNode hoveredNode) { ParentNode root = (ParentNode) NodeUtil.getNearestParentOfType(parent, RTreeNode.class); if (root != null) { INode child = ProfileVariables.browseChild(root, ""); if(child == null) { throw new NullPointerException("Scenegraph child node was not found: " + ""); } INode existing = NodeUtil.getChildById(child, DistrictNetworkHoverInfoStyle.HOVER_INFO_DEFERRED); if (existing == null) { if (child instanceof ParentNode) { existing = ((ParentNode) child).addNode(DistrictNetworkHoverInfoStyle.HOVER_INFO_DEFERRED, DeferredRenderingNode.class); ((DeferredRenderingNode)existing).setZIndex(Integer.MAX_VALUE); } else { throw new NodeException("Cannot claim child node for non-parent-node " + child); } } } activeNode.set(hoveredNode); repaint(); } }