From 2d7ddf66d6c50d01edd86ba618ead6b5423282f8 Mon Sep 17 00:00:00 2001 From: Tuukka Lehtonen Date: Mon, 11 Nov 2019 00:28:20 +0200 Subject: [PATCH] Fixed rendering of district hover info node to always stay inside editor Previously hovering on top of things near the edge of the editor would place the hover info box partially outside of the canvas. With these changes, the info box always stays within the canvas, given that the canvas is large enough to show the box in the first place. gitlab #44 Change-Id: I0869358687c9ac12dcf6fb54c9d39594d7ab6241 (cherry picked from commit 900f0e33431c1e3ea7d94d73cbf4d21f533dac26) --- .../nodes/DistrictNetworkHoverInfoNode.java | 323 +++++++++++------- 1 file changed, 200 insertions(+), 123 deletions(-) diff --git a/org.simantics.district.network.ui/src/org/simantics/district/network/ui/nodes/DistrictNetworkHoverInfoNode.java b/org.simantics.district.network.ui/src/org/simantics/district/network/ui/nodes/DistrictNetworkHoverInfoNode.java index 07ad2eed..24b7f589 100644 --- a/org.simantics.district.network.ui/src/org/simantics/district/network/ui/nodes/DistrictNetworkHoverInfoNode.java +++ b/org.simantics.district.network.ui/src/org/simantics/district/network/ui/nodes/DistrictNetworkHoverInfoNode.java @@ -2,14 +2,16 @@ 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.Comparator; +import java.util.Arrays; import java.util.List; -import java.util.Optional; +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.maps.MapScalingTransform; @@ -21,138 +23,213 @@ 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 Font FONT = new Font("Tahoma", Font.PLAIN, (int)(DPIUtil.upscale(9) * MapScalingTransform.getScaleY() + 0.5)); - - private static final long serialVersionUID = 1L; - - public static final String NODE_KEY = "DISTRICT_NETWORK_HOVER_INFO"; - - private static final int W1 = 50; - private static final int W2 = 30; - private static final int PAD = 5; - - private List labels; - - @SuppressWarnings("unused") - private Point2D origin; - - private boolean hover = false; - - private Point2D mousePosition; - - 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; - AffineTransform ot = g.getTransform(); - Font of = g.getFont(); - doRender(g); - g.setFont(of); - g.setTransform(ot); - } - - private void doRender(Graphics2D g) { - AffineTransform tt = getTransform(); - g.transform(tt); - if (mousePosition == null) - return; - g.translate(mousePosition.getX(), mousePosition.getY()); - //g.translate(origin.getX(), origin.getY()); - double scale = 1.5 / g.getTransform().getScaleX(); - g.scale(scale, scale); - - g.setFont(FONT); - double rowHeight = g.getFontMetrics().getHeight() * 1.1; - - // let's calculate the max width - Optional max = labels.stream().map(t -> g.getFontMetrics().stringWidth((String) t.c2)).max(Comparator.naturalOrder()); - int width = max.orElse(10); + 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, + (int)(DPIUtil.upscale(9) * MapScalingTransform.getScaleY() + 0.5)); + + @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.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 = DPIUtil.upscale( 1.25 / 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); - int totalHeight = (int)Math.round(rowHeight * labels.size()); - g.fillRect(-(W1 + PAD + W2 + 5), -(totalHeight + (int)Math.round(rowHeight)), (W1 + PAD + W2 + width + 10), totalHeight + 5); - - g.setColor(Color.BLACK); - - for (Tuple3 t : labels) { - g.translate(0.f, -rowHeight); - - if (t.c0 != null) { - g.drawString((String) t.c0, -(W1 + PAD + W2), 0.f); - } - - if (t.c1 != null) { - int width1 = g.getFontMetrics().stringWidth((String) t.c1); - g.drawString((String) t.c1, - width1, 0.f); - } - - if (t.c2 != null) { - g.drawString((String) t.c2, PAD, 0.f); - } - } - } - - @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); - } + 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: " + ""); -- 2.45.2