Commit | Line | Data |
---|---|---|
739b9fec AM |
1 | /******************************************************************************* |
2 | * Copyright (c) 2016 EfficiOS Inc., Alexandre Montplaisir | |
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.eclipse.tracecompass.internal.provisional.tmf.ui.views.timegraph2.swtjfx; | |
11 | ||
12 | import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; | |
13 | ||
14 | import java.util.ArrayList; | |
15 | import java.util.Collection; | |
16 | import java.util.List; | |
17 | import java.util.Set; | |
18 | import java.util.stream.Collectors; | |
19 | import java.util.stream.DoubleStream; | |
20 | import java.util.stream.IntStream; | |
21 | import java.util.stream.Stream; | |
22 | ||
23 | import org.eclipse.core.runtime.IProgressMonitor; | |
24 | import org.eclipse.core.runtime.IStatus; | |
25 | import org.eclipse.core.runtime.Status; | |
26 | import org.eclipse.core.runtime.jobs.Job; | |
27 | import org.eclipse.jdt.annotation.Nullable; | |
28 | import org.eclipse.swt.SWT; | |
29 | import org.eclipse.swt.custom.SashForm; | |
30 | import org.eclipse.swt.widgets.Composite; | |
31 | import org.eclipse.swt.widgets.Display; | |
32 | import org.eclipse.tracecompass.internal.provisional.tmf.core.views.timegraph2.ITimeGraphModelRenderProvider; | |
33 | import org.eclipse.tracecompass.internal.provisional.tmf.core.views.timegraph2.TimeGraphModelRender; | |
34 | import org.eclipse.tracecompass.internal.provisional.tmf.core.views.timegraph2.TimeGraphStateInterval; | |
35 | import org.eclipse.tracecompass.internal.provisional.tmf.core.views.timegraph2.TimeGraphTreeRender; | |
36 | import org.eclipse.tracecompass.internal.provisional.tmf.ui.views.timegraph2.TimeGraphModelViewer; | |
37 | import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace; | |
38 | import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager; | |
39 | ||
40 | import com.google.common.annotations.VisibleForTesting; | |
41 | import com.google.common.collect.Lists; | |
42 | ||
43 | import javafx.beans.value.ChangeListener; | |
44 | import javafx.embed.swt.FXCanvas; | |
45 | import javafx.event.EventHandler; | |
46 | import javafx.geometry.Insets; | |
47 | import javafx.scene.Group; | |
48 | import javafx.scene.Node; | |
49 | import javafx.scene.Scene; | |
50 | import javafx.scene.canvas.Canvas; | |
51 | import javafx.scene.canvas.GraphicsContext; | |
52 | import javafx.scene.control.Label; | |
53 | import javafx.scene.control.ScrollPane; | |
54 | import javafx.scene.control.ScrollPane.ScrollBarPolicy; | |
55 | import javafx.scene.input.MouseEvent; | |
56 | import javafx.scene.layout.Pane; | |
57 | import javafx.scene.layout.StackPane; | |
58 | import javafx.scene.layout.VBox; | |
59 | import javafx.scene.paint.Color; | |
60 | import javafx.scene.shape.Rectangle; | |
61 | import javafx.scene.shape.StrokeLineCap; | |
62 | ||
63 | /** | |
64 | * Viewer for the {@link SwtJfxTimeGraphView}, encapsulating all the view's | |
65 | * controls. | |
66 | * | |
67 | * Its contents consist of: | |
68 | * | |
69 | * TODO update this to its final form | |
70 | * <pre> | |
71 | * SashForm fBaseControl (parent is passed from the view) | |
72 | * + FXCanvas | |
73 | * | + ScrollPane | |
74 | * | + TreeView (?), contains the list of threads | |
75 | * + FXCanvas | |
76 | * + ScrollPane, will contain the time graph area | |
77 | * + Pane, gets resized to very large horizontal size to represent the whole trace range | |
78 | * + Canvas, canvas children are tiled on the Pane to show the content of one Render each | |
79 | * + Canvas | |
80 | * + ... | |
81 | * </pre> | |
82 | * | |
83 | * Both ScrolledPanes's vertical scrollbars are bound together, so that they | |
84 | * scroll together. | |
85 | * | |
86 | * @author Alexandre Montplaisir | |
87 | */ | |
88 | public class SwtJfxTimeGraphViewer extends TimeGraphModelViewer { | |
89 | ||
90 | private static final double MAX_CANVAS_WIDTH = 2000.0; | |
91 | private static final double MAX_CANVAS_HEIGHT = 2000.0; | |
92 | ||
93 | // ------------------------------------------------------------------------ | |
94 | // Style definitions | |
95 | // (Could eventually be moved to separate .css file?) | |
96 | // ------------------------------------------------------------------------ | |
97 | ||
98 | private static final Color BACKGROUD_LINES_COLOR = checkNotNull(Color.LIGHTBLUE); | |
99 | private static final String BACKGROUND_STYLE = "-fx-background-color: rgba(255, 255, 255, 255);"; //$NON-NLS-1$ | |
100 | ||
101 | private static final double SELECTION_STROKE_WIDTH = 1; | |
102 | private static final Color SELECTION_STROKE_COLOR = checkNotNull(Color.BLUE); | |
103 | private static final Color SELECTION_FILL_COLOR = checkNotNull(Color.LIGHTBLUE.deriveColor(0, 1.2, 1, 0.4)); | |
104 | ||
105 | private static final int LABEL_SIDE_MARGIN = 10; | |
106 | ||
107 | // ------------------------------------------------------------------------ | |
108 | // Class fields | |
109 | // ------------------------------------------------------------------------ | |
110 | ||
111 | private final SelectionContext fSelectionCtx = new SelectionContext(); | |
112 | private final ScrollingContext fScrollingCtx = new ScrollingContext(); | |
113 | ||
114 | private final LatestJobExecutor fJobExecutor = new LatestJobExecutor(); | |
115 | ||
116 | private final SashForm fBaseControl; | |
117 | ||
118 | private final FXCanvas fTreeFXCanvas; | |
119 | private final FXCanvas fTimeGraphFXCanvas; | |
120 | ||
121 | private final Pane fTreePane; | |
122 | private final ScrollPane fTreeScrollPane; | |
123 | private final Pane fTimeGraphPane; | |
124 | private final ScrollPane fTimeGraphScrollPane; | |
125 | ||
126 | /* | |
127 | * Children of the time graph pane are split into groups, so we can easily | |
128 | * redraw or add only some of them. | |
129 | */ | |
130 | private final Group fTimeGraphStatesLayer; | |
131 | private final Group fTimeGraphSelectionLayer; | |
132 | // TODO Layers for markers, arrows | |
133 | ||
134 | private final Rectangle fSelectionRect; | |
135 | private final Rectangle fOngoingSelectionRect; | |
136 | ||
137 | /** | |
138 | * Height of individual entries (text + states), including padding. | |
139 | * | |
140 | * TODO Make this configurable (vertical zoom feature) | |
141 | */ | |
142 | private static final double ENTRY_HEIGHT = 20; | |
143 | ||
144 | ||
145 | /** Current zoom level */ | |
146 | private double fNanosPerPixel = 1.0; | |
147 | ||
148 | ||
149 | /** | |
150 | * Constructor | |
151 | * | |
152 | * @param parent | |
153 | * Parent SWT composite | |
154 | */ | |
155 | public SwtJfxTimeGraphViewer(Composite parent, ITimeGraphModelRenderProvider provider) { | |
156 | super(provider); | |
157 | ||
158 | // TODO Convert this sash to JavaFX too? | |
159 | fBaseControl = new SashForm(parent, SWT.NONE); | |
160 | ||
161 | fTreeFXCanvas = new FXCanvas(fBaseControl, SWT.NONE); | |
162 | fTimeGraphFXCanvas = new FXCanvas(fBaseControl, SWT.NONE); | |
163 | ||
164 | // TODO Base on time-alignment | |
165 | fBaseControl.setWeights(new int[] { 15, 85 }); | |
166 | ||
167 | // -------------------------------------------------------------------- | |
168 | // Prepare the tree part's scene graph | |
169 | // -------------------------------------------------------------------- | |
170 | ||
171 | fTreePane = new Pane(); | |
172 | ||
173 | fTreeScrollPane = new ScrollPane(fTreePane); | |
174 | /* We only show the time graph's vertical scrollbar */ | |
175 | fTreeScrollPane.setVbarPolicy(ScrollBarPolicy.NEVER); | |
176 | fTreeScrollPane.setHbarPolicy(ScrollBarPolicy.ALWAYS); | |
177 | ||
178 | // -------------------------------------------------------------------- | |
179 | // Prepare the time graph's part scene graph | |
180 | // -------------------------------------------------------------------- | |
181 | ||
182 | fSelectionRect = new Rectangle(); | |
183 | fOngoingSelectionRect = new Rectangle(); | |
184 | ||
185 | Stream.of(fSelectionRect, fOngoingSelectionRect).forEach(rect -> { | |
186 | rect.setStroke(SELECTION_STROKE_COLOR); | |
187 | rect.setStrokeWidth(SELECTION_STROKE_WIDTH); | |
188 | rect.setStrokeLineCap(StrokeLineCap.ROUND); | |
189 | rect.setFill(SELECTION_FILL_COLOR); | |
190 | }); | |
191 | ||
192 | fTimeGraphStatesLayer = new Group(); | |
193 | fTimeGraphSelectionLayer = new Group(fSelectionRect, fOngoingSelectionRect); | |
194 | ||
195 | fTimeGraphPane = new Pane(fTimeGraphStatesLayer, fTimeGraphSelectionLayer); | |
196 | fTimeGraphPane.setStyle(BACKGROUND_STYLE); | |
197 | fTimeGraphPane.addEventHandler(MouseEvent.MOUSE_PRESSED, fSelectionCtx.fMousePressedEventHandler); | |
198 | fTimeGraphPane.addEventHandler(MouseEvent.MOUSE_DRAGGED, fSelectionCtx.fMouseDraggedEventHandler); | |
199 | fTimeGraphPane.addEventHandler(MouseEvent.MOUSE_RELEASED, fSelectionCtx.fMouseReleasedEventHandler); | |
200 | ||
201 | /* | |
202 | * We control the width of the time graph pane programatically, so | |
203 | * ensure that calls to setPrefWidth set the actual width right away. | |
204 | */ | |
205 | fTimeGraphPane.minWidthProperty().bind(fTimeGraphPane.prefWidthProperty()); | |
206 | fTimeGraphPane.maxWidthProperty().bind(fTimeGraphPane.prefWidthProperty()); | |
207 | ||
208 | /* | |
209 | * Ensure the time graph pane is always exactly the same vertical size | |
210 | * as the tree pane, so they remain aligned. | |
211 | */ | |
212 | fTimeGraphPane.minHeightProperty().bind(fTreePane.heightProperty()); | |
213 | fTimeGraphPane.prefHeightProperty().bind(fTreePane.heightProperty()); | |
214 | fTimeGraphPane.maxHeightProperty().bind(fTreePane.heightProperty()); | |
215 | ||
216 | fTimeGraphScrollPane = new ScrollPane(fTimeGraphPane); | |
217 | fTimeGraphScrollPane.setVbarPolicy(ScrollBarPolicy.ALWAYS); | |
218 | fTimeGraphScrollPane.setHbarPolicy(ScrollBarPolicy.ALWAYS); | |
219 | ||
220 | // fTimeGraphScrollPane.viewportBoundsProperty().addListener(fScrollingCtx.fHScrollChangeListener); | |
221 | fTimeGraphScrollPane.setOnMouseEntered(fScrollingCtx.fMouseEnteredEventHandler); | |
222 | fTimeGraphScrollPane.setOnMouseExited(fScrollingCtx.fMouseExitedEventHandler); | |
223 | fTimeGraphScrollPane.hvalueProperty().addListener(fScrollingCtx.fHScrollChangeListener); | |
224 | ||
225 | /* Synchronize the two scrollpanes' vertical scroll bars together */ | |
226 | fTreeScrollPane.vvalueProperty().bindBidirectional(fTimeGraphScrollPane.vvalueProperty()); | |
227 | ||
228 | // -------------------------------------------------------------------- | |
229 | // Hook the parts into the SWT window | |
230 | // -------------------------------------------------------------------- | |
231 | ||
232 | fTreeFXCanvas.setScene(new Scene(fTreeScrollPane)); | |
233 | fTimeGraphFXCanvas.setScene(new Scene(fTimeGraphScrollPane)); | |
234 | ||
235 | /* | |
236 | * Initially populate the viewer with the context of the current trace. | |
237 | */ | |
238 | ITmfTrace trace = TmfTraceManager.getInstance().getActiveTrace(); | |
239 | getSignalingContext().initializeForTrace(trace); | |
240 | } | |
241 | ||
242 | // ------------------------------------------------------------------------ | |
243 | // Test accessors | |
244 | // ------------------------------------------------------------------------ | |
245 | ||
246 | @VisibleForTesting | |
247 | protected Pane getTimeGraphPane() { | |
248 | return fTimeGraphPane; | |
249 | } | |
250 | ||
251 | @VisibleForTesting | |
252 | protected ScrollPane getTimeGraphScrollPane() { | |
253 | return fTimeGraphScrollPane; | |
254 | } | |
255 | ||
256 | // ------------------------------------------------------------------------ | |
257 | // Operations | |
258 | // ------------------------------------------------------------------------ | |
259 | ||
260 | @Override | |
261 | protected void seekVisibleRangeImpl(long visibleWindowStartTime, long visibleWindowEndTime) { | |
262 | final long fullTimeGraphStart = getFullTimeGraphStartTime(); | |
263 | final long fullTimeGraphEnd = getFullTimeGraphEndTime(); | |
264 | ||
265 | /* Update the zoom level */ | |
266 | long windowTimeRange = visibleWindowEndTime - visibleWindowStartTime; | |
267 | double timeGraphWidth = fTimeGraphScrollPane.getWidth(); | |
268 | fNanosPerPixel = windowTimeRange / timeGraphWidth; | |
269 | ||
270 | double timeGraphAreaWidth = timestampToPaneXPos(fullTimeGraphEnd) - timestampToPaneXPos(fullTimeGraphStart); | |
271 | if (timeGraphAreaWidth < 1.0) { | |
272 | // FIXME | |
273 | return; | |
274 | } | |
275 | ||
276 | double newValue; | |
277 | if (visibleWindowStartTime == fullTimeGraphStart) { | |
278 | newValue = fTimeGraphScrollPane.getHmin(); | |
279 | } else if (visibleWindowEndTime == fullTimeGraphEnd) { | |
280 | newValue = fTimeGraphScrollPane.getHmax(); | |
281 | } else { | |
282 | // FIXME Not aligned perfectly yet, see how the scrolling | |
283 | // listener does it? | |
284 | long targetTs = (visibleWindowStartTime + visibleWindowEndTime) / 2; | |
285 | double xPos = timestampToPaneXPos(targetTs); | |
286 | newValue = xPos / timeGraphAreaWidth; | |
287 | } | |
288 | ||
289 | fTimeGraphPane.setPrefWidth(timeGraphAreaWidth); | |
290 | fTimeGraphScrollPane.setHvalue(newValue); | |
291 | } | |
292 | ||
293 | @Override | |
294 | protected void paintAreaImpl(ITmfTrace trace, long windowStartTime, long windowEndTime) { | |
295 | final long fullTimeGraphStart = getFullTimeGraphStartTime(); | |
296 | final long fullTimeGraphEnd = getFullTimeGraphEndTime(); | |
297 | ||
298 | /* | |
299 | * Get the current target width of the viewer, so we know at which | |
300 | * resolution we must do state system queries. | |
301 | * | |
302 | * Yes! We can query the size of visible components outside of the UI | |
303 | * thread! Praise the JavaFX! | |
304 | */ | |
305 | long treePaneWidth = Math.round(fTreeScrollPane.getWidth()); | |
306 | ||
307 | long windowTimeRange = windowEndTime - windowStartTime; | |
308 | ||
309 | Job job = new Job("Time Graph Update") { | |
310 | @Override | |
311 | protected IStatus run(@Nullable IProgressMonitor monitor) { | |
312 | IProgressMonitor mon = checkNotNull(monitor); | |
313 | ||
314 | /* Apply the configuration options to the render provider */ | |
315 | ITimeGraphModelRenderProvider renderProvider = getModelRenderProvider(); | |
316 | renderProvider.setConfiguredTimeRange(windowStartTime, windowEndTime); | |
317 | ||
318 | /* | |
319 | * Request the needed renders and prepare the corresponding | |
320 | * canvases. We target at most one "window width" before and | |
321 | * after the current window, clamped by the trace's start and | |
322 | * end. | |
323 | */ | |
324 | final long renderingStartTime = Math.max(fullTimeGraphStart, windowStartTime - windowTimeRange); | |
325 | final long renderingEndTime = Math.min(fullTimeGraphEnd, windowEndTime + windowTimeRange); | |
326 | final long renderTimeRange = (long) (MAX_CANVAS_WIDTH * fNanosPerPixel); | |
327 | ||
328 | List<TimeGraphModelRender> renders = new ArrayList<>(); | |
329 | long renderStart = renderingStartTime - renderTimeRange; | |
330 | // TODO Find a way to streamize/parallelize this loop? | |
331 | do { | |
332 | renderStart += renderTimeRange; | |
333 | long renderEnd = Math.min(renderStart + renderTimeRange, renderingEndTime); | |
334 | // FIXME Even with /10 we sometimes get holes in the view. Subpixel rendering? | |
335 | // Needs to be debugged/tested further. | |
336 | long resolution = Math.max(1, Math.round(fNanosPerPixel / 10)); | |
337 | ||
338 | System.out.printf("requesting render from %,d to %,d, resolution=%d%n", | |
339 | renderStart, renderEnd, resolution); | |
340 | ||
341 | TimeGraphModelRender render = renderProvider.getRender(trace, | |
342 | renderStart, renderEnd, resolution, monitor); | |
343 | renders.add(render); | |
344 | } while ((renderStart + renderTimeRange) < renderingEndTime); | |
345 | ||
346 | if (mon.isCanceled()) { | |
347 | /* Job was cancelled, no need to update the UI */ | |
348 | System.out.println("job was cancelled before it could end"); | |
349 | return Status.CANCEL_STATUS; | |
350 | } | |
351 | ||
352 | if (renders.isEmpty()) { | |
353 | /* Nothing to show yet, keep the view empty */ | |
354 | return Status.OK_STATUS; | |
355 | } | |
356 | ||
357 | /* Prepare the time graph part */ | |
358 | Node timeGraphContents = prepareTimeGraphContents(renders); | |
359 | ||
360 | /* Prepare the tree part */ | |
361 | Node treeContents = prepareTreeContents(renders.get(0).getTreeRender(), treePaneWidth); | |
362 | ||
363 | if (mon.isCanceled()) { | |
364 | /* Job was cancelled, no need to update the UI */ | |
365 | System.out.println("job was cancelled before it could end"); | |
366 | return Status.CANCEL_STATUS; | |
367 | } | |
368 | ||
369 | /* Update the view! */ | |
370 | Display.getDefault().syncExec( () -> { | |
371 | fTreePane.getChildren().clear(); | |
372 | fTreePane.getChildren().add(treeContents); | |
373 | ||
374 | fTimeGraphStatesLayer.getChildren().clear(); | |
375 | fTimeGraphStatesLayer.getChildren().add(timeGraphContents); | |
376 | }); | |
377 | ||
378 | return Status.OK_STATUS; | |
379 | } | |
380 | }; | |
381 | ||
382 | fJobExecutor.schedule(job); | |
383 | } | |
384 | ||
385 | @Override | |
386 | protected void drawSelectionImpl(long selectionStartTime, long selectionEndTime) { | |
387 | double xStart = timestampToPaneXPos(selectionStartTime); | |
388 | double xEnd = timestampToPaneXPos(selectionEndTime); | |
389 | double xWidth = xEnd - xStart; | |
390 | ||
391 | fSelectionRect.setX(xStart); | |
392 | fSelectionRect.setY(0); | |
393 | fSelectionRect.setWidth(xWidth); | |
394 | fSelectionRect.setHeight(fTimeGraphPane.getHeight()); | |
395 | ||
396 | fSelectionRect.setVisible(true); | |
397 | } | |
398 | ||
399 | // ------------------------------------------------------------------------ | |
400 | // Methods related to the Tree area | |
401 | // ------------------------------------------------------------------------ | |
402 | ||
403 | private static Node prepareTreeContents(TimeGraphTreeRender treeRender, double paneWidth) { | |
404 | /* Prepare the tree element objects */ | |
405 | List<Label> treeElements = treeRender.getAllTreeElements().stream() | |
406 | // TODO Put as a real tree. TreeView ? | |
407 | .map(elem -> new Label(elem.getName())) | |
408 | .peek(label -> { | |
409 | label.setPrefHeight(ENTRY_HEIGHT); | |
410 | label.setPadding(new Insets(0, LABEL_SIDE_MARGIN, 0, LABEL_SIDE_MARGIN)); | |
411 | /* | |
412 | * Re-set the solid background for the labels, so we do not | |
413 | * see the background lines through. | |
414 | */ | |
415 | label.setStyle(BACKGROUND_STYLE); | |
416 | }) | |
417 | .collect(Collectors.toList()); | |
418 | ||
419 | VBox treeElemsBox = new VBox(); // Change to TreeView eventually ? | |
420 | treeElemsBox.getChildren().addAll(treeElements); | |
421 | ||
422 | /* Prepare the Canvases with the horizontal alignment lines */ | |
423 | List<Canvas> canvases = new ArrayList<>(); | |
424 | int maxEntriesPerCanvas = (int) (MAX_CANVAS_HEIGHT / ENTRY_HEIGHT); | |
425 | Lists.partition(treeElements, maxEntriesPerCanvas).forEach(subList -> { | |
426 | int nbElements = subList.size(); | |
427 | double height = nbElements * ENTRY_HEIGHT; | |
428 | ||
429 | Canvas canvas = new Canvas(paneWidth, height); | |
430 | drawBackgroundLines(canvas, ENTRY_HEIGHT); | |
431 | canvas.setCache(true); | |
432 | canvases.add(canvas); | |
433 | }); | |
434 | VBox canvasBox = new VBox(); | |
435 | canvasBox.getChildren().addAll(canvases); | |
436 | ||
437 | /* Put the background Canvas and the Tree View into their containers */ | |
438 | StackPane stackPane = new StackPane(canvasBox, treeElemsBox); | |
439 | stackPane.setStyle(BACKGROUND_STYLE); | |
440 | return stackPane; | |
441 | } | |
442 | ||
443 | // ------------------------------------------------------------------------ | |
444 | // Methods related to the Time Graph area | |
445 | // ------------------------------------------------------------------------ | |
446 | ||
447 | private Node prepareTimeGraphContents(List<TimeGraphModelRender> renders) { | |
448 | Set<Node> canvases = renders.stream() | |
449 | .parallel() // order doesn't matter here | |
450 | .flatMap(render -> getCanvasesForRender(render).stream()) | |
451 | .collect(Collectors.toSet()); | |
452 | ||
453 | return new Group(canvases); | |
454 | } | |
455 | ||
456 | /** | |
457 | * Get the vertically-tiled Canvas's for a single render. They will | |
458 | * be already relocated correctly, so the collection's order does not | |
459 | * matter. | |
460 | * | |
461 | * @param render | |
462 | * The render | |
463 | * @return The vertical set of canvases | |
464 | */ | |
465 | private Collection<Canvas> getCanvasesForRender(TimeGraphModelRender render) { | |
466 | List<List<TimeGraphStateInterval>> stateIntervals = render.getStateIntervals(); | |
467 | /* The canvas will be put on the Pane at this offset */ | |
468 | final double xOffset = timestampToPaneXPos(render.getStartTime()); | |
469 | final double xEnd = timestampToPaneXPos(render.getEndTime()); | |
470 | final double canvasWidth = xEnd - xOffset; | |
471 | final int maxEntriesPerCanvas = (int) (MAX_CANVAS_HEIGHT / ENTRY_HEIGHT); | |
472 | ||
473 | /* | |
474 | * Split the full list of intervals into smaller partitions, and draw | |
475 | * one Canvas per partition. | |
476 | */ | |
477 | List<Canvas> canvases = new ArrayList<>(); | |
478 | double yOffset = 0; | |
479 | List<List<List<TimeGraphStateInterval>>> partitionedIntervals = | |
480 | Lists.partition(stateIntervals, maxEntriesPerCanvas); | |
481 | for (int i = 0; i < partitionedIntervals.size(); i++) { | |
482 | /* "states" represent the subset of intervals to draw on this Canvas */ | |
483 | List<List<TimeGraphStateInterval>> states = partitionedIntervals.get(i); | |
484 | final double canvasHeight = ENTRY_HEIGHT * states.size(); | |
485 | ||
486 | Canvas canvas = new Canvas(canvasWidth, canvasHeight); | |
487 | drawBackgroundLines(canvas, ENTRY_HEIGHT); | |
488 | drawStates(states, canvas.getGraphicsContext2D(), xOffset); | |
489 | ||
490 | // System.out.println("relocating canvas of size + (" + canvasWidth + ", " + canvasHeight + ") to " + xOffset + ", " + yOffset); | |
491 | canvas.relocate(xOffset, yOffset); | |
492 | canvas.setCache(true); // TODO Test? | |
493 | canvases.add(canvas); | |
494 | ||
495 | yOffset += canvasHeight; | |
496 | } | |
497 | return canvases; | |
498 | } | |
499 | ||
500 | private void drawStates(List<List<TimeGraphStateInterval>> stateIntervalsToDraw, GraphicsContext gc, double xOffset) { | |
501 | IntStream.range(0, stateIntervalsToDraw.size()).forEach(index -> { | |
502 | /* | |
503 | * The base (top) of each full-thickness rectangle object we will | |
504 | * draw for this entry | |
505 | */ | |
506 | final double xBase = index * ENTRY_HEIGHT; | |
507 | ||
508 | List<TimeGraphStateInterval> intervals = stateIntervalsToDraw.get(index); | |
509 | for (TimeGraphStateInterval interval : intervals) { | |
510 | try { | |
511 | /* | |
512 | * These coordinates are relative to the canvas itself, so | |
513 | * we need to substract the value of the offset of the | |
514 | * canvas relative to the Pane. | |
515 | */ | |
516 | final double xStart = timestampToPaneXPos(interval.getStartEvent().getTimestamp()) - xOffset; | |
517 | final double xEnd = timestampToPaneXPos(interval.getEndEvent().getTimestamp()) - xOffset; | |
518 | final double xWidth = Math.max(1.0, xEnd - xStart); | |
519 | ||
520 | double yStart, yHeight; | |
521 | switch (interval.getLineThickness()) { | |
522 | case NORMAL: | |
523 | default: | |
524 | yStart = xBase + 4; | |
525 | yHeight = ENTRY_HEIGHT - 4; | |
526 | break; | |
527 | case SMALL: | |
528 | yStart = xBase + 8; | |
529 | yHeight = ENTRY_HEIGHT - 8; | |
530 | break; | |
531 | } | |
532 | ||
533 | gc.setFill(JfxColorFactory.getColorFromDef(interval.getColorDefinition())); | |
534 | gc.fillRect(xStart, yStart, xWidth, yHeight); | |
535 | ||
536 | } catch (IllegalArgumentException iae) { // TODO Temp | |
537 | System.out.println("out of bounds interval:" + interval.toString()); | |
538 | continue; | |
539 | } | |
540 | ||
541 | // TODO Paint the state's name if applicable | |
542 | } | |
543 | }); | |
544 | ||
545 | } | |
546 | ||
547 | // ------------------------------------------------------------------------ | |
548 | // Mouse event listeners | |
549 | // ------------------------------------------------------------------------ | |
550 | ||
551 | /** | |
552 | * Class encapsulating the time range selection, related drawing and | |
553 | * listeners. | |
554 | */ | |
555 | private class SelectionContext { | |
556 | ||
557 | private boolean fOngoingSelection; | |
558 | private double fMouseOriginX; | |
559 | ||
560 | public final EventHandler<MouseEvent> fMousePressedEventHandler = e -> { | |
561 | if (e.isShiftDown() || | |
562 | e.isControlDown() || | |
563 | e.isSecondaryButtonDown() || | |
564 | e.isMiddleButtonDown()) { | |
565 | /* Do other things! */ | |
566 | // TODO! | |
567 | return; | |
568 | } | |
569 | ||
570 | if (fOngoingSelection) { | |
571 | return; | |
572 | } | |
573 | ||
574 | /* Remove the current selection, if there is one */ | |
575 | fSelectionRect.setVisible(false); | |
576 | ||
577 | fMouseOriginX = e.getX(); | |
578 | ||
579 | fOngoingSelectionRect.setX(fMouseOriginX); | |
580 | fOngoingSelectionRect.setY(0); | |
581 | fOngoingSelectionRect.setWidth(0); | |
582 | fOngoingSelectionRect.setHeight(fTimeGraphPane.getHeight()); | |
583 | ||
584 | fOngoingSelectionRect.setVisible(true); | |
585 | ||
586 | e.consume(); | |
587 | ||
588 | fOngoingSelection = true; | |
589 | }; | |
590 | ||
591 | public final EventHandler<MouseEvent> fMouseDraggedEventHandler = e -> { | |
592 | double newX = e.getX(); | |
593 | double offsetX = newX - fMouseOriginX; | |
594 | ||
595 | if (offsetX > 0) { | |
596 | fOngoingSelectionRect.setX(fMouseOriginX); | |
597 | fOngoingSelectionRect.setWidth(offsetX); | |
598 | } else { | |
599 | fOngoingSelectionRect.setX(newX); | |
600 | fOngoingSelectionRect.setWidth(-offsetX); | |
601 | } | |
602 | ||
603 | e.consume(); | |
604 | }; | |
605 | ||
606 | public final EventHandler<MouseEvent> fMouseReleasedEventHandler = e -> { | |
607 | fOngoingSelectionRect.setVisible(false); | |
608 | ||
609 | e.consume(); | |
610 | ||
611 | /* Send a time range selection signal for the currently selected time range */ | |
612 | double startX = Math.max(0, fOngoingSelectionRect.getX()); | |
613 | // FIXME Possible glitch when selecting backwards outside of the window | |
614 | double endX = Math.min(fTimeGraphPane.getWidth(), startX + fOngoingSelectionRect.getWidth()); | |
615 | long tsStart = paneXPosToTimestamp(startX); | |
616 | long tsEnd = paneXPosToTimestamp(endX); | |
617 | ||
618 | getSignalingContext().sendTimeRangeSelectionUpdate(tsStart, tsEnd); | |
619 | ||
620 | fOngoingSelection = false; | |
621 | }; | |
622 | } | |
623 | ||
624 | /** | |
625 | * Class encapsulating the scrolling operations of the time graph pane. | |
626 | * | |
627 | * The mouse entered/exited handlers ensure only the scrollpane being | |
628 | * interacted by the user is the one sending the synchronization signals. | |
629 | */ | |
630 | private class ScrollingContext { | |
631 | ||
632 | private boolean fUserActionOngoing = false; | |
633 | ||
634 | private final EventHandler<MouseEvent> fMouseEnteredEventHandler = e -> { | |
635 | fUserActionOngoing = true; | |
636 | }; | |
637 | ||
638 | private final EventHandler<MouseEvent> fMouseExitedEventHandler = e -> { | |
639 | fUserActionOngoing = false; | |
640 | }; | |
641 | ||
642 | /** | |
643 | * Listener for the horizontal scrollbar changes | |
644 | */ | |
645 | private final ChangeListener<Object> fHScrollChangeListener = (observable, oldValue, newValue) -> { | |
646 | if (!fUserActionOngoing) { | |
647 | System.out.println("Listener triggered but inactive"); | |
648 | return; | |
649 | } | |
650 | ||
651 | System.out.println("Change listener triggered, oldval=" + oldValue.toString() + ", newval=" + newValue.toString()); | |
652 | ||
653 | /* | |
654 | * Determine the X position represented by the left edge of the pane | |
655 | */ | |
656 | double hmin = fTimeGraphScrollPane.getHmin(); | |
657 | double hmax = fTimeGraphScrollPane.getHmax(); | |
658 | double hvalue = fTimeGraphScrollPane.getHvalue(); | |
659 | double contentWidth = fTimeGraphPane.getLayoutBounds().getWidth(); | |
660 | double viewportWidth = fTimeGraphScrollPane.getViewportBounds().getWidth(); | |
661 | double hoffset = Math.max(0, contentWidth - viewportWidth) * (hvalue - hmin) / (hmax - hmin); | |
662 | ||
663 | /* | |
664 | * Convert the positions of the left and right edges to timestamps, | |
665 | * and send a window range update signal | |
666 | */ | |
667 | long tsStart = paneXPosToTimestamp(hoffset); | |
668 | long tsEnd = paneXPosToTimestamp(hoffset + viewportWidth); | |
669 | ||
670 | System.out.printf("Offset: %.1f, width: %.1f %n", hoffset, viewportWidth); | |
671 | System.out.printf("Sending visible range update: %,d to %,d%n", tsStart, tsEnd); | |
672 | ||
673 | getSignalingContext().sendVisibleWindowRangeUpdate(tsStart, tsEnd); | |
674 | }; | |
675 | } | |
676 | ||
677 | // ------------------------------------------------------------------------ | |
678 | // Common utils | |
679 | // ------------------------------------------------------------------------ | |
680 | ||
681 | private static void drawBackgroundLines(Canvas canvas, double entryHeight) { | |
682 | double width = canvas.getWidth(); | |
683 | int nbLines = (int) (canvas.getHeight() / entryHeight); | |
684 | ||
685 | ||
686 | GraphicsContext gc = canvas.getGraphicsContext2D(); | |
687 | gc.save(); | |
688 | ||
689 | gc.setStroke(BACKGROUD_LINES_COLOR); | |
690 | gc.setLineWidth(1); | |
691 | /* average+2 gives the best-looking output */ | |
692 | DoubleStream.iterate((ENTRY_HEIGHT / 2) + 2, i -> i + entryHeight).limit(nbLines).forEach(yPos -> { | |
693 | gc.strokeLine(0, yPos, width, yPos); | |
694 | }); | |
695 | ||
696 | gc.restore(); | |
697 | } | |
698 | ||
699 | private double timestampToPaneXPos(long timestamp) { | |
700 | return timestampToPaneXPos(timestamp, getFullTimeGraphStartTime(), getFullTimeGraphEndTime(), fNanosPerPixel); | |
701 | } | |
702 | ||
703 | @VisibleForTesting | |
704 | public static double timestampToPaneXPos(long timestamp, long start, long end, double nanosPerPixel) { | |
705 | if (timestamp < start) { | |
706 | throw new IllegalArgumentException(timestamp + " is smaller than trace start time " + start); //$NON-NLS-1$ | |
707 | } | |
708 | if (timestamp > end) { | |
709 | throw new IllegalArgumentException(timestamp + " is greater than trace end time " + end); //$NON-NLS-1$ | |
710 | } | |
711 | ||
712 | double traceTimeRange = end - start; | |
713 | double timeStampRatio = (timestamp - start) / traceTimeRange; | |
714 | ||
715 | long fullTraceWidthInPixels = (long) (traceTimeRange / nanosPerPixel); | |
716 | double xPos = fullTraceWidthInPixels * timeStampRatio; | |
717 | return Math.round(xPos); | |
718 | } | |
719 | ||
720 | private long paneXPosToTimestamp(double x) { | |
721 | return paneXPosToTimestamp(x, fTimeGraphPane.getWidth(), getFullTimeGraphStartTime(), fNanosPerPixel); | |
722 | } | |
723 | ||
724 | @VisibleForTesting | |
725 | public static long paneXPosToTimestamp(double x, double totalWidth, long startTimestamp, double nanosPerPixel) { | |
726 | if (x < 0.0 || totalWidth < 1.0 || x > totalWidth) { | |
727 | throw new IllegalArgumentException("Invalid position arguments: pos=" + x + ", width=" + totalWidth); | |
728 | } | |
729 | ||
730 | long ts = Math.round(x * nanosPerPixel); | |
731 | return ts + startTimestamp; | |
732 | } | |
733 | ||
734 | } |