]> gerrit.simantics Code Review - simantics/district.git/blob - org.simantics.district.network.ui/src/org/simantics/district/network/ui/nodes/DistrictNetworkHoverInfoNode.java
Fixed rendering of district hover info node to always stay inside editor
[simantics/district.git] / org.simantics.district.network.ui / src / org / simantics / district / network / ui / nodes / DistrictNetworkHoverInfoNode.java
1 package org.simantics.district.network.ui.nodes;
2
3 import java.awt.Color;
4 import java.awt.Font;
5 import java.awt.FontMetrics;
6 import java.awt.Graphics2D;
7 import java.awt.geom.AffineTransform;
8 import java.awt.geom.Point2D;
9 import java.awt.geom.Rectangle2D;
10 import java.util.Arrays;
11 import java.util.List;
12 import java.util.Objects;
13 import java.util.concurrent.atomic.AtomicReference;
14 import java.util.function.ToIntFunction;
15
16 import org.simantics.district.network.ui.styles.DistrictNetworkHoverInfoStyle;
17 import org.simantics.maps.MapScalingTransform;
18 import org.simantics.scenegraph.INode;
19 import org.simantics.scenegraph.NodeException;
20 import org.simantics.scenegraph.ParentNode;
21 import org.simantics.scenegraph.g2d.G2DNode;
22 import org.simantics.scenegraph.g2d.G2DParentNode;
23 import org.simantics.scenegraph.g2d.nodes.spatial.RTreeNode;
24 import org.simantics.scenegraph.profile.common.ProfileVariables;
25 import org.simantics.scenegraph.utils.DPIUtil;
26 import org.simantics.scenegraph.utils.GeometryUtils;
27 import org.simantics.scenegraph.utils.NodeUtil;
28 import org.simantics.scl.runtime.Lists;
29 import org.simantics.scl.runtime.tuple.Tuple3;
30
31 public class DistrictNetworkHoverInfoNode extends G2DNode implements HoverSensitiveNode, DeferredNode {
32
33     private static final long serialVersionUID = 1L;
34
35     public static final String NODE_KEY = "DISTRICT_NETWORK_HOVER_INFO";
36
37     private static final int PAD = 15;
38
39     private List<Tuple3> labels;
40
41     private Font font = new Font(
42             Font.SANS_SERIF,
43             Font.PLAIN,
44             (int)(DPIUtil.upscale(9) * MapScalingTransform.getScaleY() + 0.5));
45
46     @SuppressWarnings("unused")
47     private Point2D origin;
48
49     private boolean hover = false;
50
51     private Point2D mousePosition;
52
53     /**
54      * For rendering and clipping the hover info base rectangle.
55      */
56     private Rectangle2D bgRect = new Rectangle2D.Double();
57
58     private static AtomicReference<G2DParentNode> activeNode = new AtomicReference<>();
59
60     @Override
61     public void render(Graphics2D g) {
62         ParentNode<?> root = (ParentNode<?>) NodeUtil.getNearestParentOfType(this, RTreeNode.class);
63         DeferredRenderingNode deferred = root != null ? (DeferredRenderingNode) root.getNode(DistrictNetworkHoverInfoStyle.HOVER_INFO_DEFERRED) : null;
64         if (deferred != null)
65             deferred.deferNode(g.getTransform(), this);
66         else
67             renderDeferred(g);
68     }
69
70     @Override
71     public void renderDeferred(Graphics2D g) {
72         if (!hover || activeNode.get() == null)
73             return;
74         if (labels.isEmpty() || mousePosition == null)
75             return;
76         AffineTransform ot = g.getTransform();
77         Font of = g.getFont();
78         doRender(g);
79         g.setFont(of);
80         g.setTransform(ot);
81     }
82
83     private ToIntFunction<String> widthMeasurer(FontMetrics fm) {
84         return s -> fm.stringWidth(s);
85     }
86
87     private void doRender(Graphics2D g) {
88         AffineTransform tt = getTransform();
89         g.transform(tt);
90
91         g.translate(mousePosition.getX(), mousePosition.getY());
92         //g.translate(origin.getX(), origin.getY());
93         double scale = DPIUtil.upscale( 1.25 / GeometryUtils.getScale(g.getTransform()) );
94         g.scale(scale, scale);
95
96         g.setFont(font);
97
98         FontMetrics fm = g.getFontMetrics();
99         ToIntFunction<String> sizer = widthMeasurer(fm);
100
101         double rowHeight = fm.getHeight() * 1.1;
102
103         // Let's calculate the max widths of each column.
104         // However, the last label is assumed to be a title row.
105         List<Tuple3> values = labels.subList(0, labels.size() - 1);
106         Tuple3 title = labels.get(labels.size() - 1);
107
108         int maxLabelWidth = values.stream().map(t -> (String) t.c0).mapToInt(sizer).max().orElse(PAD);
109         int[] valueWidths = values.stream().map(t -> (String) t.c1).mapToInt(sizer).toArray();
110         int maxValueWidth = Arrays.stream(valueWidths).max().orElse(PAD);
111         int maxUnitWidth = values.stream().map(t -> (String) t.c2).mapToInt(sizer).max().orElse(PAD);
112
113         String titleString = 
114                 (Objects.toString(title.c0, "")
115                         + " " + Objects.toString(title.c1, "")
116                         + " " + Objects.toString(title.c2, ""))
117                 .trim();
118         int titleWidth = sizer.applyAsInt(titleString);
119
120         int titleRowWidth = PAD + titleWidth + PAD;
121         int maxValueRowWidth = PAD + maxLabelWidth + PAD + maxValueWidth + PAD + maxUnitWidth + PAD;
122
123         int totalWidth = Math.max(maxValueRowWidth, titleRowWidth);
124         int totalHeight = (int) Math.round((rowHeight) * labels.size());
125
126         double minX = -(PAD + maxLabelWidth + PAD + maxValueWidth + PAD);
127         double minY = -(totalHeight + (int)Math.round(rowHeight));
128
129         bgRect.setRect(minX, minY, totalWidth, totalHeight + PAD);
130         confineIn(bgRect, g.getClip().getBounds2D());
131
132         g.setColor(Color.WHITE);
133         g.fill(bgRect);
134
135         g.setColor(Color.DARK_GRAY);
136         g.draw(bgRect);
137
138         g.setColor(Color.BLACK);
139         g.translate(bgRect.getMinX() - minX, bgRect.getMinY() - minY);
140
141         int rows = values.size();
142         double y = -rowHeight;
143
144         float labelX = -(maxLabelWidth + PAD + maxValueWidth + PAD);
145         float unitX = 0;
146
147         for (int i = 0; i < rows; ++i) {
148             Tuple3 t = values.get(i);
149             float ty = (float) y;
150
151             if (t.c0 != null && ((String) t.c0).length() > 0) {
152                 g.drawString((String) t.c0, labelX, ty);
153             }
154             if (t.c1 != null && ((String) t.c1).length() > 0) {
155                 g.drawString((String) t.c1, -(valueWidths[i] + PAD), ty);
156             }
157             if (t.c2 != null && ((String) t.c2).length() > 0) {
158                 g.drawString((String) t.c2, unitX, ty);
159             }
160
161             y -= rowHeight;
162         }
163
164         if (!titleString.trim().isEmpty()) {
165             int titleX = (int) (labelX - PAD + (bgRect.getWidth() - titleWidth) * 0.5);
166             g.drawString(titleString, titleX, (float) y);
167         }
168     }
169
170     private Rectangle2D confineIn(Rectangle2D r, Rectangle2D bounds) {
171         // Handle right/bottom overrun
172         double maxX = Math.min(r.getMaxX(), bounds.getMaxX());
173         double maxY = Math.min(r.getMaxY(), bounds.getMaxY());
174         r.setFrame(
175                 maxX - r.getWidth(),
176                 maxY - r.getHeight(),
177                 r.getWidth(),
178                 r.getHeight());
179
180         // Handle left/top overrun
181         r.setFrame(
182                 Math.max(r.getMinX(), bounds.getMinX()),
183                 Math.max(r.getMinY(), bounds.getMinY()),
184                 r.getWidth(),
185                 r.getHeight());
186
187         return r;
188     }
189
190     @Override
191     public Rectangle2D getBoundsInLocal() {
192         return null;
193     }
194
195     @SuppressWarnings("unchecked")
196     public void setLabels(List<Tuple3> list) {
197         this.labels = Lists.reverse(list);
198     }
199
200     public void setOrigin(Point2D origin) {
201         this.origin = origin;
202     }
203
204     @Override
205     public boolean hover(boolean hover, boolean isConnectionTool) {
206 //        hover = hover && activeNode.updateAndGet(current -> current == null ? this : current) == this;
207         boolean changed = hover != this.hover;
208         this.hover = hover;
209
210 //        if (changed) {
211 //            if (!hover) activeNode.updateAndGet(current -> current == this ? null : current);
212 //            repaint();
213 //        }
214
215         return changed;
216     }
217
218     @Override
219     public void setMousePosition(Point2D p) {
220         mousePosition = p;
221     }
222
223     @Override
224     public void delete() {
225         super.delete();
226         activeNode.set(null);
227     }
228
229     public void hover2(G2DParentNode hoveredNode) {
230         ParentNode<?> root = (ParentNode<?>) NodeUtil.getNearestParentOfType(parent, RTreeNode.class);
231         if (root != null) {
232
233             INode child = ProfileVariables.browseChild(root, "");
234             if(child == null) {
235                 throw new NullPointerException("Scenegraph child node was not found: " + "");
236             }
237
238             INode existing = NodeUtil.getChildById(child, DistrictNetworkHoverInfoStyle.HOVER_INFO_DEFERRED);
239             if (existing == null) {
240                 if (child instanceof ParentNode<?>) {
241                     existing = ((ParentNode<?>) child).addNode(DistrictNetworkHoverInfoStyle.HOVER_INFO_DEFERRED, DeferredRenderingNode.class);
242                     ((DeferredRenderingNode)existing).setZIndex(Integer.MAX_VALUE);
243                 } else {
244                     throw new NodeException("Cannot claim child node for non-parent-node " + child);
245                 }
246             }
247         }
248
249         activeNode.set(hoveredNode);
250         repaint();
251     }
252
253 }