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