Commit | Line | Data |
---|---|---|
c879c4db AM |
1 | /* |
2 | * Copyright (C) 2017 EfficiOS Inc., Alexandre Montplaisir <alexmonthy@efficios.com> | |
3 | * | |
4 | * All rights reserved. This program and the accompanying materials are | |
5 | * made available under the terms of the Eclipse Public License v1.0 which | |
6 | * accompanies this distribution, and is available at | |
7 | * http://www.eclipse.org/legal/epl-v10.html | |
8 | */ | |
9 | ||
10 | package org.lttng.scope.tmf2.views.ui.timeline.widgets.timegraph; | |
11 | ||
12 | import static java.util.Objects.requireNonNull; | |
13 | ||
14 | import java.util.Arrays; | |
15 | import java.util.Map; | |
16 | import java.util.Objects; | |
17 | ||
18 | import org.eclipse.jdt.annotation.Nullable; | |
19 | import org.lttng.scope.tmf2.views.core.TimeRange; | |
20 | import org.lttng.scope.tmf2.views.core.timegraph.model.render.LineThickness; | |
21 | import org.lttng.scope.tmf2.views.core.timegraph.model.render.states.TimeGraphStateInterval; | |
22 | import org.lttng.scope.tmf2.views.ui.jfx.CountingGridPane; | |
23 | import org.lttng.scope.tmf2.views.ui.jfx.JfxColorFactory; | |
24 | import org.lttng.scope.tmf2.views.ui.jfx.JfxUtils; | |
25 | import org.lttng.scope.tmf2.views.ui.timeline.DebugOptions; | |
26 | ||
27 | import com.google.common.base.MoreObjects; | |
28 | ||
29 | import javafx.application.Platform; | |
30 | import javafx.geometry.Point2D; | |
31 | import javafx.scene.Node; | |
32 | import javafx.scene.control.Tooltip; | |
33 | import javafx.scene.input.MouseButton; | |
34 | import javafx.scene.paint.Paint; | |
35 | import javafx.scene.shape.Rectangle; | |
36 | import javafx.scene.text.Text; | |
37 | ||
38 | /** | |
39 | * {@link Rectangle} object used to draw states in the timegraph. It attaches | |
40 | * the {@link TimeGraphStateInterval} that represents this state. | |
41 | * | |
42 | * @author Alexandre Montplaisir | |
43 | */ | |
44 | public class StateRectangle extends Rectangle { | |
45 | ||
46 | private final TimeGraphWidget fWidget; | |
47 | private final int fEntryIndex; | |
48 | private final TimeGraphStateInterval fInterval; | |
49 | ||
50 | private @Nullable transient Paint fBaseColor; | |
51 | private @Nullable transient Paint fSelectedColor; | |
52 | ||
53 | private @Nullable Tooltip fTooltip = null; | |
54 | ||
55 | /** | |
56 | * Constructor | |
57 | * | |
58 | * @param viewer | |
59 | * The viewer in which the rectangle will be placed | |
60 | * @param interval | |
61 | * The source interval model object | |
62 | * @param entryIndex | |
63 | * The index of the entry to which this state belongs. | |
64 | */ | |
65 | public StateRectangle(TimeGraphWidget viewer, TimeGraphStateInterval interval, int entryIndex) { | |
66 | fWidget = viewer; | |
67 | fEntryIndex = entryIndex; | |
68 | fInterval = interval; | |
69 | ||
70 | /* | |
71 | * It is possible, especially when re-opening already-indexed traces, | |
72 | * that the indexer and the state system do not report the same | |
73 | * start/end times. Make sure to clamp the interval's bounds to the | |
74 | * valid values. | |
75 | */ | |
76 | TimeRange traceRange = viewer.getControl().getViewContext().getCurrentTraceFullRange(); | |
77 | long traceStart = traceRange.getStart(); | |
78 | long intervalStart = interval.getStartTime(); | |
79 | double xStart = viewer.timestampToPaneXPos(Math.max(traceStart, intervalStart)); | |
80 | ||
81 | long traceEnd = traceRange.getEnd(); | |
82 | long intervalEndTime = interval.getEndTime(); | |
83 | double xEnd = viewer.timestampToPaneXPos(Math.min(traceEnd, intervalEndTime)); | |
84 | ||
85 | double width = Math.max(1.0, xEnd - xStart) + 1.0; | |
86 | double height = getHeightFromThickness(interval.getLineThickness().get()); | |
87 | double y = computeY(height); | |
88 | ||
89 | xProperty().bind(JfxUtils.ZERO_PROPERTY); | |
90 | setLayoutX(xStart); | |
91 | setY(y); | |
92 | setWidth(width); | |
93 | setHeight(height); | |
94 | ||
95 | double opacity = viewer.getDebugOptions().stateIntervalOpacity.get(); | |
96 | setOpacity(opacity); | |
97 | ||
98 | updatePaint(); | |
99 | ||
100 | /* Set initial selection state and selection listener. */ | |
101 | if (this.equals(viewer.getSelectedState())) { | |
102 | setSelected(true); | |
103 | viewer.setSelectedState(this, false); | |
104 | } else { | |
105 | setSelected(false); | |
106 | } | |
107 | ||
108 | setOnMouseClicked(e -> { | |
109 | if (e.getButton() != MouseButton.PRIMARY) { | |
110 | return; | |
111 | } | |
112 | viewer.setSelectedState(this, true); | |
113 | }); | |
114 | ||
115 | /* | |
116 | * Initialize the tooltip when the mouse enters the rectangle, if it was | |
117 | * not done previously. | |
118 | */ | |
119 | setOnMouseEntered(e -> generateTooltip()); | |
120 | } | |
121 | ||
122 | private void generateTooltip() { | |
123 | if (fTooltip != null) { | |
124 | return; | |
125 | } | |
126 | TooltipContents ttContents = new TooltipContents(fWidget.getDebugOptions()); | |
127 | ttContents.addTooltipRow(Messages.statePropertyElement, fInterval.getTreeElement().getName()); | |
128 | ttContents.addTooltipRow(Messages.statePropertyStateName, fInterval.getStateName()); | |
129 | ttContents.addTooltipRow(Messages.statePropertyStartTime, fInterval.getStartTime()); | |
130 | ttContents.addTooltipRow(Messages.statePropertyEndTime, fInterval.getEndTime()); | |
131 | ttContents.addTooltipRow(Messages.statePropertyDuration, fInterval.getDuration() + " ns"); //$NON-NLS-1$ | |
132 | /* Add rows corresponding to the properties from the interval */ | |
133 | Map<String, String> properties = fInterval.getProperties(); | |
134 | properties.forEach((k, v) -> ttContents.addTooltipRow(k, v)); | |
135 | ||
136 | Tooltip tt = new Tooltip(); | |
137 | tt.setGraphic(ttContents); | |
138 | Tooltip.install(this, tt); | |
139 | fTooltip = tt; | |
140 | } | |
141 | ||
142 | /** | |
143 | * Return the model interval representing this state | |
144 | * | |
145 | * @return The interval model object | |
146 | */ | |
147 | public TimeGraphStateInterval getStateInterval() { | |
148 | return fInterval; | |
149 | } | |
150 | ||
151 | public void updatePaint() { | |
152 | /* Update the color */ | |
153 | /* Set a special paint for multi-state intervals */ | |
154 | if (fInterval.isMultiState()) { | |
155 | Paint multiStatePaint = fWidget.getDebugOptions().multiStatePaint.get(); | |
156 | fBaseColor = multiStatePaint; | |
157 | fSelectedColor = multiStatePaint; | |
158 | } else { | |
159 | fBaseColor = JfxColorFactory.getColorFromDef(fInterval.getColorDefinition().get()); | |
160 | fSelectedColor = JfxColorFactory.getDerivedColorFromDef(fInterval.getColorDefinition().get()); | |
161 | } | |
162 | setFill(fBaseColor); | |
163 | ||
164 | /* Update the line thickness */ | |
165 | LineThickness lt = fInterval.getLineThickness().get(); | |
166 | double height = getHeightFromThickness(lt); | |
167 | setHeight(height); | |
168 | /* We need to adjust the y position too */ | |
169 | setY(computeY(height)); | |
170 | } | |
171 | ||
172 | /** | |
173 | * Compute the Y property (the Y position of the *top* of the rectangle) | |
174 | * this rectangle should have on its pane. This takes into consideration the | |
175 | * entry it belongs to, as well as its target height. | |
176 | * | |
177 | * For example, if the line thickness of the rectangle changes, its Y has to | |
178 | * be recomputed so that the rectangle remains centered on its entry line. | |
179 | * | |
180 | * This method does not change the yProperty of the rectangle. | |
181 | */ | |
182 | private double computeY(double height) { | |
183 | double yOffset = (TimeGraphWidget.ENTRY_HEIGHT - height) / 2; | |
184 | double y = fEntryIndex * TimeGraphWidget.ENTRY_HEIGHT + yOffset; | |
185 | return y; | |
186 | } | |
187 | ||
188 | public void setSelected(boolean isSelected) { | |
189 | if (isSelected) { | |
190 | setFill(fSelectedColor); | |
191 | } else { | |
192 | setFill(fBaseColor); | |
193 | hideTooltip(); | |
194 | } | |
195 | } | |
196 | ||
197 | public void showTooltip(boolean beginning) { | |
198 | generateTooltip(); | |
199 | Tooltip tt = requireNonNull(fTooltip); | |
200 | ||
201 | /* | |
202 | * Show the tooltip first, then move it to the correct location. It | |
203 | * needs to be shown for its getWidth() etc. to be populated. | |
204 | */ | |
205 | tt.show(this, 0, 0); | |
206 | ||
207 | Point2D position; | |
208 | if (beginning) { | |
209 | /* Align to the bottom-left of the rectangle, left-aligned. */ | |
210 | /* Yes, it needs to be getX() here (0), not getLayoutX(). */ | |
211 | position = this.localToScreen(getX(), getY() + getHeight()); | |
212 | } else { | |
213 | /* Align to the bottom-right of the rectangle, right-aligned */ | |
214 | position = this.localToScreen(getX() + getWidth() - tt.getWidth(), getY() + getHeight()); | |
215 | } | |
216 | ||
217 | tt.setAnchorX(position.getX()); | |
218 | tt.setAnchorY(position.getY()); | |
219 | } | |
220 | ||
221 | public void hideTooltip() { | |
222 | Tooltip tt = fTooltip; | |
223 | if (tt != null) { | |
224 | Platform.runLater(() -> { | |
225 | tt.hide(); | |
226 | }); | |
227 | } | |
228 | } | |
229 | ||
230 | @Override | |
231 | protected void finalize() { | |
232 | hideTooltip(); | |
233 | } | |
234 | ||
235 | public static double getHeightFromThickness(LineThickness lt) { | |
236 | switch (lt) { | |
237 | case NORMAL: | |
238 | default: | |
239 | return TimeGraphWidget.ENTRY_HEIGHT - 4; | |
240 | case SMALL: | |
241 | return TimeGraphWidget.ENTRY_HEIGHT - 8; | |
242 | case TINY: | |
243 | return TimeGraphWidget.ENTRY_HEIGHT - 12; | |
244 | } | |
245 | } | |
246 | ||
247 | @Override | |
248 | public int hashCode() { | |
249 | return Objects.hash(fWidget, fInterval); | |
250 | } | |
251 | ||
252 | @Override | |
253 | public boolean equals(@Nullable Object obj) { | |
254 | if (this == obj) { | |
255 | return true; | |
256 | } | |
257 | if (obj == null) { | |
258 | return false; | |
259 | } | |
260 | if (getClass() != obj.getClass()) { | |
261 | return false; | |
262 | } | |
263 | StateRectangle other = (StateRectangle) obj; | |
264 | return Objects.equals(fWidget, other.fWidget) | |
265 | && Objects.equals(fInterval, other.fInterval); | |
266 | } | |
267 | ||
268 | @Override | |
269 | public String toString() { | |
270 | return MoreObjects.toStringHelper(this) | |
271 | .add("interval", fInterval) //$NON-NLS-1$ | |
272 | .toString(); | |
273 | } | |
274 | ||
275 | private static class TooltipContents extends CountingGridPane { | |
276 | ||
277 | private final DebugOptions fOpts; | |
278 | ||
279 | public TooltipContents(DebugOptions opts) { | |
280 | fOpts = opts; | |
281 | } | |
282 | ||
283 | public void addTooltipRow(Object... objects) { | |
284 | Node[] labels = Arrays.stream(objects) | |
285 | .map(Object::toString) | |
286 | .map(Text::new) | |
287 | .peek(text -> { | |
288 | text.fontProperty().bind(fOpts.toolTipFont); | |
289 | text.fillProperty().bind(fOpts.toolTipFontFill); | |
290 | }) | |
291 | .toArray(Node[]::new); | |
292 | appendRow(labels); | |
293 | } | |
294 | } | |
295 | } |