]> gerrit.simantics Code Review - simantics/district.git/commitdiff
Fixed rendering of district hover info node to always stay inside editor 87/3487/2
authorTuukka Lehtonen <tuukka.lehtonen@semantum.fi>
Sun, 10 Nov 2019 22:28:20 +0000 (00:28 +0200)
committerTuukka Lehtonen <tuukka.lehtonen@semantum.fi>
Sun, 10 Nov 2019 22:36:22 +0000 (22:36 +0000)
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

org.simantics.district.network.ui/src/org/simantics/district/network/ui/nodes/DistrictNetworkHoverInfoNode.java

index 07ad2eedb4df1decd66f4ac0e965a76a67d6039c..24b7f5898c0dd0a06970ce1b28147c1b623120d2 100644 (file)
@@ -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<Tuple3> labels;
-
-       @SuppressWarnings("unused")
-       private Point2D origin;
-
-       private boolean hover = false;
-
-       private Point2D mousePosition;
-       
-       private static AtomicReference<G2DParentNode> 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<Integer> 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<Tuple3> 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<G2DParentNode> 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<String> 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<String> 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<Tuple3> 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<Tuple3> 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<Tuple3> 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: " + "");