Fix time graph widget initialization
[deliverable/tracecompass.git] / tmf / org.lttng.scope.tmf2.views.ui / src / org / lttng / scope / tmf2 / views / ui / timeline / widgets / timegraph / TimeGraphWidget.java
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.lttng.scope.tmf2.views.ui.timeline.widgets.timegraph;
11
12 import static java.util.Objects.requireNonNull;
13
14 import java.util.Collection;
15 import java.util.Timer;
16 import java.util.concurrent.CountDownLatch;
17 import java.util.concurrent.TimeUnit;
18
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.lttng.scope.tmf2.views.core.NestingBoolean;
22 import org.lttng.scope.tmf2.views.core.TimeRange;
23 import org.lttng.scope.tmf2.views.core.timegraph.control.TimeGraphModelControl;
24 import org.lttng.scope.tmf2.views.core.timegraph.model.provider.ITimeGraphModelProvider;
25 import org.lttng.scope.tmf2.views.core.timegraph.model.render.tree.TimeGraphTreeRender;
26 import org.lttng.scope.tmf2.views.core.timegraph.view.TimeGraphModelView;
27 import org.lttng.scope.tmf2.views.ui.timeline.DebugOptions;
28 import org.lttng.scope.tmf2.views.ui.timeline.ITimelineWidget;
29 import org.lttng.scope.tmf2.views.ui.timeline.TimelineView;
30 import org.lttng.scope.tmf2.views.ui.timeline.widgets.timegraph.layer.TimeGraphArrowLayer;
31 import org.lttng.scope.tmf2.views.ui.timeline.widgets.timegraph.layer.TimeGraphBackgroundLayer;
32 import org.lttng.scope.tmf2.views.ui.timeline.widgets.timegraph.layer.TimeGraphDrawnEventLayer;
33 import org.lttng.scope.tmf2.views.ui.timeline.widgets.timegraph.layer.TimeGraphSelectionLayer;
34 import org.lttng.scope.tmf2.views.ui.timeline.widgets.timegraph.layer.TimeGraphStateLayer;
35 import org.lttng.scope.tmf2.views.ui.timeline.widgets.timegraph.toolbar.ViewerToolBar;
36
37 import com.google.common.annotations.VisibleForTesting;
38
39 import javafx.application.Platform;
40 import javafx.beans.property.DoubleProperty;
41 import javafx.beans.property.SimpleDoubleProperty;
42 import javafx.beans.value.ChangeListener;
43 import javafx.concurrent.Task;
44 import javafx.event.EventHandler;
45 import javafx.geometry.Orientation;
46 import javafx.scene.Group;
47 import javafx.scene.Parent;
48 import javafx.scene.control.ScrollPane;
49 import javafx.scene.control.ScrollPane.ScrollBarPolicy;
50 import javafx.scene.control.SplitPane;
51 import javafx.scene.control.ToolBar;
52 import javafx.scene.input.InputEvent;
53 import javafx.scene.input.ScrollEvent;
54 import javafx.scene.layout.BorderPane;
55 import javafx.scene.layout.Pane;
56 import javafx.scene.paint.Color;
57 import javafx.scene.shape.Rectangle;
58
59 /**
60 * Viewer for the {@link TimelineView}, encapsulating all the view's
61 * controls.
62 *
63 * Both ScrolledPanes's vertical scrollbars are bound together, so that they
64 * scroll together.
65 *
66 * @author Alexandre Montplaisir
67 */
68 public class TimeGraphWidget extends TimeGraphModelView implements ITimelineWidget {
69
70 // ------------------------------------------------------------------------
71 // Style definitions
72 // (Could eventually be moved to separate .css file?)
73 // ------------------------------------------------------------------------
74
75 public static final Color BACKGROUD_LINES_COLOR = requireNonNull(Color.LIGHTBLUE);
76
77 private static final String BACKGROUND_STYLE = "-fx-background-color: rgba(255, 255, 255, 255);"; //$NON-NLS-1$
78
79 private static final int LABEL_SIDE_MARGIN = 10;
80
81 /**
82 * Height of individual entries (text + states), including padding.
83 *
84 * TODO Make this configurable (vertical zoom feature)
85 */
86 public static final double ENTRY_HEIGHT = 20;
87
88 /** Minimum allowed zoom level, in nanos per pixel */
89 private static final double ZOOM_LIMIT = 1.0;
90
91 // ------------------------------------------------------------------------
92 // Instance fields
93 // ------------------------------------------------------------------------
94
95 private final DebugOptions fDebugOptions = new DebugOptions();
96
97 private final ScrollingContext fScrollingCtx = new ScrollingContext();
98 private final ZoomActions fZoomActions = new ZoomActions();
99
100 /*
101 * Children of the time graph pane are split into groups, so we can easily
102 * redraw or add only some of them.
103 */
104 // TODO Layer for bookmarks
105 private final TimeGraphBackgroundLayer fBackgroundLayer;
106 private final TimeGraphStateLayer fStateLayer;
107 private final TimeGraphArrowLayer fArrowLayer;
108 private final TimeGraphDrawnEventLayer fDrawnEventLayer;
109 private final TimeGraphSelectionLayer fSelectionLayer;
110 private final Group fTimeGraphLoadingOverlayGroup;
111
112 private final LatestTaskExecutor fTaskExecutor = new LatestTaskExecutor();
113
114 private final NestingBoolean fHScrollListenerStatus;
115
116 private final BorderPane fBasePane;
117 private final ToolBar fToolBar;
118 private final SplitPane fSplitPane;
119
120 private final TimeGraphWidgetTreeArea fTreeArea;
121
122 private final Pane fTimeGraphPane;
123 private final ScrollPane fTimeGraphScrollPane;
124
125 private final LoadingOverlay fTimeGraphLoadingOverlay;
126
127 private final Timer fUiUpdateTimer = new Timer();
128 private final PeriodicRedrawTask fUiUpdateTimerTask = new PeriodicRedrawTask(this);
129
130 private volatile TimeGraphTreeRender fLatestTreeRender = TimeGraphTreeRender.EMPTY_RENDER;
131
132 /** Current zoom level */
133 private final DoubleProperty fNanosPerPixel = new SimpleDoubleProperty(1.0);
134
135 /**
136 * Constructor
137 *
138 * @param control
139 * The control for this widget. See
140 * {@link TimeGraphModelControl}.
141 * @param hScrollListenerStatus
142 * If the hscroll property of this widget's scrollpane is bound
143 * with others (possibly through the
144 * {@link ITimelineWidget#getTimeBasedScrollPane()} method), then
145 * a common {@link NestingBoolean} should be used to track
146 * requests to disable the hscroll listener.
147 * <p>
148 * If the widget is to be used stand-alone, then you can pass a "
149 * <code>new NestingBoolean()</code> " that only this view will
150 * use.
151 */
152 public TimeGraphWidget(TimeGraphModelControl control, NestingBoolean hScrollListenerStatus) {
153 super(control);
154 fHScrollListenerStatus = hScrollListenerStatus;
155
156 // --------------------------------------------------------------------
157 // Prepare the tree part's scene graph
158 // --------------------------------------------------------------------
159
160 fTreeArea = new TimeGraphWidgetTreeArea(ENTRY_HEIGHT, getControl().getModelRenderProvider().traceProperty());
161
162 // --------------------------------------------------------------------
163 // Prepare the time graph's part scene graph
164 // --------------------------------------------------------------------
165
166 fTimeGraphLoadingOverlay = new LoadingOverlay(fDebugOptions);
167 fTimeGraphLoadingOverlayGroup = new Group(fTimeGraphLoadingOverlay);
168
169 fTimeGraphPane = new Pane();
170 fBackgroundLayer = new TimeGraphBackgroundLayer(this, new Group());
171 fStateLayer = new TimeGraphStateLayer(this, new Group());
172 fArrowLayer = new TimeGraphArrowLayer(this, new Group());
173 fDrawnEventLayer = new TimeGraphDrawnEventLayer(this, new Group());
174 fSelectionLayer = new TimeGraphSelectionLayer(this, new Group());
175
176 /*
177 * The order of the layers is important here, it will go from back to
178 * front.
179 */
180 fTimeGraphPane.getChildren().addAll(fBackgroundLayer.getParentGroup(),
181 fStateLayer.getParentGroup(),
182 fStateLayer.getLabelGroup(),
183 fArrowLayer.getParentGroup(),
184 fDrawnEventLayer.getParentGroup(),
185 fSelectionLayer.getParentGroup(),
186 fTimeGraphLoadingOverlayGroup);
187
188 fTimeGraphPane.setStyle(BACKGROUND_STYLE);
189
190 /*
191 * We control the width of the time graph pane programmatically, so
192 * ensure that calls to setPrefWidth set the actual width right away.
193 */
194 fTimeGraphPane.minWidthProperty().bind(fTimeGraphPane.prefWidthProperty());
195 fTimeGraphPane.maxWidthProperty().bind(fTimeGraphPane.prefWidthProperty());
196
197 /*
198 * Ensure the time graph pane is always exactly the same vertical size
199 * as the tree pane, so they remain aligned.
200 */
201
202 fTimeGraphPane.minHeightProperty().bind(fTreeArea.currentHeightProperty());
203 fTimeGraphPane.prefHeightProperty().bind(fTreeArea.currentHeightProperty());
204 fTimeGraphPane.maxHeightProperty().bind(fTreeArea.currentHeightProperty());
205
206 /*
207 * Setup clipping on the timegraph pane, meaning its children outside of its
208 * actual boundary should not be rendered. For example, when the tree gets
209 * collapsed, data for hidden entries should be hidden too.
210 */
211 Rectangle clipRect = new Rectangle();
212 clipRect.setX(0);
213 clipRect.setY(0);
214 clipRect.widthProperty().bind(fTimeGraphPane.widthProperty());
215 clipRect.heightProperty().bind(fTimeGraphPane.heightProperty());
216 fTimeGraphPane.setClip(clipRect);
217
218 /*
219 * Set the loading overlay's size to always follow the size of the pane.
220 */
221 fTimeGraphLoadingOverlay.widthProperty().bind(fTimeGraphPane.widthProperty());
222 fTimeGraphLoadingOverlay.heightProperty().bind(fTimeGraphPane.heightProperty());
223
224 fTimeGraphScrollPane = new ScrollPane(fTimeGraphPane);
225 fTimeGraphScrollPane.setVbarPolicy(ScrollBarPolicy.ALWAYS);
226 fTimeGraphScrollPane.setHbarPolicy(ScrollBarPolicy.ALWAYS);
227 fTimeGraphScrollPane.setFitToHeight(true);
228 fTimeGraphScrollPane.setFitToWidth(true);
229 fTimeGraphScrollPane.setPannable(true);
230
231 /*
232 * Attach the scrollbar listener
233 *
234 * TODO Move this to the timeline ?
235 */
236 fTimeGraphScrollPane.hvalueProperty().addListener(fScrollingCtx.fHScrollChangeListener);
237
238 /*
239 * Mouse scroll handlers (for zooming) are attached to the time graph
240 * itself: events let through will be used by the scrollpane as normal
241 * scroll actions.
242 */
243 fTimeGraphPane.setOnScroll(fMouseScrollListener);
244
245 /*
246 * Upon reception of any mouse/keyboard event, if there's still a drawn
247 * tooltip it should be hidden.
248 */
249 fTimeGraphPane.addEventFilter(InputEvent.ANY, e -> {
250 StateRectangle selectedState = fSelectedState;
251 if (selectedState != null) {
252 selectedState.hideTooltip();
253 }
254 /* We must not consume the event here */
255 });
256
257 /* Synchronize the two scrollpanes' vertical scroll bars together */
258 fTreeArea.getVerticalScrollBar().valueProperty().bindBidirectional(fTimeGraphScrollPane.vvalueProperty());
259
260 // --------------------------------------------------------------------
261 // Prepare the top-level area
262 // --------------------------------------------------------------------
263
264 fToolBar = new ViewerToolBar(this);
265
266 fSplitPane = new SplitPane(fTreeArea, fTimeGraphScrollPane);
267 fSplitPane.setOrientation(Orientation.HORIZONTAL);
268
269 fBasePane = new BorderPane();
270 fBasePane.setCenter(fSplitPane);
271 fBasePane.setTop(fToolBar);
272
273 /* Start the periodic redraw thread */
274 long delay = fDebugOptions.uiUpdateDelay.get();
275 fUiUpdateTimer.schedule(fUiUpdateTimerTask, delay, delay);
276 }
277
278 public TimeGraphTreeRender getLatestTreeRender() {
279 return fLatestTreeRender;
280 }
281
282 // ------------------------------------------------------------------------
283 // ITimelineWidget
284 // ------------------------------------------------------------------------
285
286 @Override
287 public String getName() {
288 return getControl().getModelRenderProvider().getName();
289 }
290
291 @Override
292 public Parent getRootNode() {
293 return fBasePane;
294 }
295
296 @Override
297 public @NonNull SplitPane getSplitPane() {
298 return fSplitPane;
299 }
300
301 @Override
302 public @NonNull ScrollPane getTimeBasedScrollPane() {
303 return fTimeGraphScrollPane;
304 }
305
306 @Override
307 public @Nullable Rectangle getSelectionRectangle() {
308 return fSelectionLayer.getSelectionRectangle();
309 }
310
311 @Override
312 public @Nullable Rectangle getOngoingSelectionRectangle() {
313 return fSelectionLayer.getOngoingSelectionRectangle();
314 }
315
316 // ------------------------------------------------------------------------
317 // Operations
318 // ------------------------------------------------------------------------
319
320 @Override
321 public void disposeImpl() {
322 /* Stop/cleanup the redraw thread */
323 fUiUpdateTimer.cancel();
324 fUiUpdateTimer.purge();
325 }
326
327 @Override
328 public void clear() {
329 Platform.runLater(() -> {
330 /*
331 * Clear the generated children of the various groups so they go
332 * back to their initial (post-constructor) state.
333 */
334 fTreeArea.clear();
335
336 fBackgroundLayer.clear();
337 fStateLayer.clear();
338 fArrowLayer.clear();
339 fDrawnEventLayer.clear();
340
341 /* Also clear whatever cached objects the viewer currently has. */
342 fLatestTreeRender = TimeGraphTreeRender.EMPTY_RENDER;
343 fUiUpdateTimerTask.forceRedraw();
344 });
345 }
346
347 @Override
348 public void seekVisibleRange(TimeRange newVisibleRange) {
349 final TimeRange fullTimeGraphRange = getViewContext().getCurrentTraceFullRange();
350
351 /* Update the zoom level */
352 long windowTimeRange = newVisibleRange.getDuration();
353 double timeGraphVisibleWidth = fTimeGraphScrollPane.getViewportBounds().getWidth();
354 if (timeGraphVisibleWidth < 100) {
355 /*
356 * The view's width is reported as 0 if the widget is not yet part of the
357 * scenegraph. Instead target a larger width so that we obtain a value of
358 * nanos-per-pixel that makes sense.
359 */
360 timeGraphVisibleWidth = 2000;
361 }
362 fNanosPerPixel.set(windowTimeRange / timeGraphVisibleWidth);
363
364 double oldTotalWidth = fTimeGraphPane.getLayoutBounds().getWidth();
365 double newTotalWidth = timestampToPaneXPos(fullTimeGraphRange.getEnd()) - timestampToPaneXPos(fullTimeGraphRange.getStart());
366 if (newTotalWidth < 1.0) {
367 // FIXME
368 return;
369 }
370
371 double newValue;
372 if (newVisibleRange.getStart() == fullTimeGraphRange.getStart()) {
373 newValue = fTimeGraphScrollPane.getHmin();
374 } else if (newVisibleRange.getEnd() == fullTimeGraphRange.getEnd()) {
375 newValue = fTimeGraphScrollPane.getHmax();
376 } else {
377 /*
378 * The "hvalue" is in reference to the beginning of the pane, not
379 * the middle point as one could think.
380 *
381 * Also note that the "scrollable distance" is not simply
382 * "timeGraphTotalWidth", it's
383 * "timeGraphTotalWidth - timeGraphVisibleWidth". The view does not
384 * allow scrolling the start and end edges up to the middle point
385 * for example.
386 *
387 * See http://stackoverflow.com/a/23518314/4227853 for a great
388 * explanation.
389 */
390 double startPos = timestampToPaneXPos(newVisibleRange.getStart());
391 newValue = startPos / (newTotalWidth - timeGraphVisibleWidth);
392 }
393
394 fHScrollListenerStatus.disable();
395 try {
396
397 /*
398 * If the zoom level changed, resize the pane and relocate its
399 * current contents. That way the "intermediate" display before the
400 * next repaint will continue showing correct data.
401 */
402 if (Math.abs(newTotalWidth - oldTotalWidth) > 0.5) {
403
404 /* Resize/reposition the state rectangles */
405 double factor = (newTotalWidth / oldTotalWidth);
406 fStateLayer.getRenderedStateRectangles().forEach(rect -> {
407 rect.setLayoutX(rect.getLayoutX() * factor);
408 rect.setWidth(rect.getWidth() * factor);
409 });
410
411 /* Reposition the text labels (don't stretch them!) */
412 fStateLayer.getRenderedStateLabels().forEach(text -> {
413 text.setX(text.getX() * factor);
414 });
415
416 /* Reposition the arrows */
417 fArrowLayer.getRenderedArrows().forEach(arrow -> {
418 arrow.setStartX(arrow.getStartX() * factor);
419 arrow.setEndX(arrow.getEndX() * factor);
420 });
421
422 /* Reposition the drawn events */
423 fDrawnEventLayer.getRenderedEvents().forEach(event -> {
424 /*
425 * Drawn events use the "translate" properties to define
426 * their position.
427 */
428 event.setTranslateX(event.getTranslateX() * factor);
429 });
430
431
432 /*
433 * Resize the pane itself. Remember min/max are bound to the
434 * "pref" width, so this will change the actual size right away.
435 */
436 fTimeGraphPane.setPrefWidth(newTotalWidth);
437 /*
438 * Since we changed the size of a child of the scrollpane, it's
439 * important to call layout() on it before setHvalue(). If we
440 * don't, the setHvalue() will apply to the old layout, and the
441 * upcoming pulse will simply revert our changes.
442 */
443 fTimeGraphScrollPane.layout();
444 }
445
446 fTimeGraphScrollPane.setHvalue(newValue);
447
448 } finally {
449 fHScrollListenerStatus.enable();
450 }
451
452 /*
453 * Redraw the current selection, as it may have moved if we changed the
454 * size of the pane.
455 */
456 redrawSelection();
457 }
458
459 /**
460 *
461 * Paint the specified view area.
462 *
463 * @param windowRange
464 * The horizontal position where the visible window currently is
465 * @param verticalPos
466 * The vertical position where the visible window currently is
467 * @param movedHorizontally
468 * If we have moved horizontally since the last redraw. May be
469 * used to skip some operations. If you are not sure say "true".
470 * @param movedVertically
471 * If we have moved vertically since the last redraw. May be used
472 * to skip some operations. If you are not sure say "true".
473 * @param taskSeqNb
474 * The sequence number of this task, used for logging only
475 */
476 void paintArea(TimeRange windowRange, VerticalPosition verticalPos,
477 boolean movedHorizontally, boolean movedVertically,
478 long taskSeqNb) {
479 final TimeRange fullTimeGraphRange = getViewContext().getCurrentTraceFullRange();
480
481 /*
482 * Request the needed renders and prepare the corresponding UI objects.
483 * We may ask for some padding on each side, clamped by the trace's
484 * start and end.
485 */
486 final long timeRangePadding = Math.round(windowRange.getDuration() * fDebugOptions.renderRangePadding.get());
487 final long renderingStartTime = Math.max(fullTimeGraphRange.getStart(), windowRange.getStart() - timeRangePadding);
488 final long renderingEndTime = Math.min(fullTimeGraphRange.getEnd(), windowRange.getEnd() + timeRangePadding);
489 final TimeRange renderingRange = TimeRange.of(renderingStartTime, renderingEndTime);
490
491 /*
492 * Start a new repaint, display the "loading" overlay. The next
493 * paint task to finish will put it back to non-visible.
494 */
495 if (getDebugOptions().isLoadingOverlayEnabled.get()) {
496 fTimeGraphLoadingOverlay.fadeIn();
497 }
498
499 Task<@Nullable Void> task = new Task<@Nullable Void>() {
500 @Override
501 protected @Nullable Void call() {
502 System.err.println("Starting paint task #" + taskSeqNb);
503
504 ITimeGraphModelProvider modelProvider = getControl().getModelRenderProvider();
505 TimeGraphTreeRender treeRender = modelProvider.getTreeRender();
506
507 if (isCancelled()) {
508 return null;
509 }
510
511 /* Prepare the tree part, if needed */
512 if (!treeRender.equals(fLatestTreeRender)) {
513 fLatestTreeRender = treeRender;
514 fTreeArea.updateTreeContents(treeRender);
515 }
516
517 if (isCancelled()) {
518 return null;
519 }
520
521 /* Paint the background. It's very quick so we can do it every time. */
522 fBackgroundLayer.drawContents(treeRender, renderingRange, verticalPos, this);
523
524 /*
525 * The state rectangles should be redrawn as soon as we move,
526 * either horizontally or vertically.
527 */
528 fStateLayer.setWindowRange(windowRange);
529 fStateLayer.drawContents(treeRender, renderingRange, verticalPos, this);
530
531 if (isCancelled()) {
532 return null;
533 }
534
535 /*
536 * Arrows and drawn events are drawn for the full vertical
537 * range. Only refetch/repaint them if we moved horizontally.
538 */
539 if (movedHorizontally) {
540 fArrowLayer.drawContents(treeRender, renderingRange, verticalPos, this);
541 fDrawnEventLayer.drawContents(treeRender, renderingRange, verticalPos, this);
542 }
543
544 if (isCancelled()) {
545 return null;
546 }
547
548 /* Painting is finished, turn off the loading overlay */
549 Platform.runLater(() -> {
550 System.err.println("fading out overlay");
551 fTimeGraphLoadingOverlay.fadeOut();
552 if (fRepaintLatch != null) {
553 fRepaintLatch.countDown();
554 }
555 });
556
557 return null;
558 }
559 };
560
561 System.err.println("Queueing task #" + taskSeqNb);
562
563 /*
564 * Attach a listener to the task to receive exceptions thrown within the
565 * task.
566 */
567 task.exceptionProperty().addListener((obs, oldVal, newVal) -> {
568 if (newVal != null) {
569 newVal.printStackTrace();
570 }
571 });
572
573 fTaskExecutor.schedule(task);
574 }
575
576 @Override
577 public void drawSelection(TimeRange selectionRange) {
578 fSelectionLayer.drawSelection(selectionRange);
579 }
580
581 private void redrawSelection() {
582 TimeRange selectionRange = getViewContext().getCurrentSelectionTimeRange();
583 drawSelection(selectionRange);
584 }
585
586 private @Nullable StateRectangle fSelectedState = null;
587
588 /**
589 * Set the selected state rectangle
590 *
591 * @param state
592 * The new selected state. It should ideally be one that's
593 * present in the scenegraph.
594 * @param deselectPrevious
595 * If the previously selected interval should be unmarked as
596 * selected.
597 */
598 public void setSelectedState(StateRectangle state, boolean deselectPrevious) {
599 @Nullable StateRectangle previousSelectedState = fSelectedState;
600 if (previousSelectedState != null) {
601 previousSelectedState.hideTooltip();
602 if (deselectPrevious) {
603 previousSelectedState.setSelected(false);
604 }
605 }
606
607 state.setSelected(true);
608 fSelectedState = state;
609 }
610
611 /**
612 * Get the currently selected state interval
613 *
614 * @return The current selected state
615 */
616 public @Nullable StateRectangle getSelectedState() {
617 return fSelectedState;
618 }
619
620 /**
621 * Return all state rectangles currently present in the timegraph.
622 *
623 * @return The rendered state rectangles
624 */
625 public Collection<StateRectangle> getRenderedStateRectangles() {
626 return fStateLayer.getRenderedStateRectangles();
627 }
628
629 // ------------------------------------------------------------------------
630 // Mouse event listeners
631 // ------------------------------------------------------------------------
632
633 /**
634 * Class encapsulating the scrolling operations of the time graph pane.
635 *
636 * The mouse entered/exited handlers ensure only the scrollpane being
637 * interacted by the user is the one sending the synchronization signals.
638 */
639 private class ScrollingContext {
640
641 /**
642 * Listener for the horizontal scrollbar changes
643 */
644 private final ChangeListener<Number> fHScrollChangeListener = (observable, oldValue, newValue) -> {
645 if (!fDebugOptions.isScrollingListenersEnabled.get()) {
646 System.out.println("HScroll event ignored due to debug option");
647 return;
648 }
649 if (!fHScrollListenerStatus.enabledProperty().get()) {
650 System.out.println("HScroll listener triggered but inactive");
651 return;
652 }
653
654 System.out.println("HScroll change listener triggered, oldval=" + oldValue.toString() + ", newval=" + newValue.toString());
655
656 /* We need to specify the new value here, or else the old one will be used */
657 TimeRange range = getTimeGraphEdgeTimestamps(newValue.doubleValue());
658
659 System.out.println("Sending visible range update: " + range.toString());
660
661 getControl().updateVisibleTimeRange(range, false);
662
663 /*
664 * We ask the control to not send this signal back to us (to avoid
665 * jitter while scrolling), but the next UI update should refresh
666 * the view accordingly.
667 *
668 * It is not our responsibility to update to this
669 * HorizontalPosition. The control will update accordingly upon
670 * managing the signal we just sent.
671 */
672 };
673 }
674
675 /**
676 * Event handler attached to the *time graph pane*, to execute zooming
677 * operations when the control key is down (otherwise, it just lets the even
678 * bubble to the ScrollPane, which will do a standard scroll).
679 */
680 private final EventHandler<ScrollEvent> fMouseScrollListener = e -> {
681 boolean forceUseMousePosition = false;
682
683 if (!e.isControlDown()) {
684 return;
685 }
686
687 if (e.isShiftDown()) {
688 forceUseMousePosition = true;
689 }
690 e.consume();
691
692 double delta = e.getDeltaY();
693 boolean zoomIn = (delta > 0.0); // false means a zoom-out
694
695 /*
696 * getX() corresponds to the X position of the mouse on the time graph.
697 * This is seriously awesome.
698 */
699 fZoomActions.zoom(zoomIn, forceUseMousePosition, e.getX());
700
701 };
702
703 // ------------------------------------------------------------------------
704 // View-specific actions
705 // These do not come from the control, but from the view itself
706 // ------------------------------------------------------------------------
707
708 /**
709 * Utils class encapsulating zoom operations
710 */
711 public class ZoomActions {
712
713 public void zoom(boolean zoomIn, boolean forceUseMousePosition, @Nullable Double mouseX) {
714 final double zoomStep = fDebugOptions.zoomStep.get();
715
716 double newScaleFactor = (zoomIn ? 1.0 * (1 + zoomStep) : 1.0 * (1 / (1 + zoomStep)));
717
718 /* Send a corresponding window-range signal to the control */
719 TimeGraphModelControl control = getControl();
720 TimeRange visibleRange = getViewContext().getCurrentVisibleTimeRange();
721
722 TimeRange currentSelection = getViewContext().getCurrentSelectionTimeRange();
723 long currentSelectionCenter = ((currentSelection.getDuration() / 2) + currentSelection.getStart());
724 boolean currentSelectionCenterIsVisible = visibleRange.contains(currentSelectionCenter);
725
726 long zoomPivot;
727 if (fDebugOptions.zoomPivotOnMousePosition.get() && mouseX != null && forceUseMousePosition) {
728 /* Pivot on mouse position */
729 zoomPivot = paneXPosToTimestamp(mouseX);
730 } else if (fDebugOptions.zoomPivotOnSelection.get() && currentSelectionCenterIsVisible) {
731 /* Pivot on current selection center */
732 zoomPivot = currentSelectionCenter;
733 } else if (fDebugOptions.zoomPivotOnMousePosition.get() && mouseX != null) {
734 /* Pivot on mouse position */
735 zoomPivot = paneXPosToTimestamp(mouseX);
736 } else {
737 /* Pivot on center of visible range */
738 zoomPivot = visibleRange.getStart() + (visibleRange.getDuration() / 2);
739 }
740
741 /* Prevent going closer than the zoom limit */
742 double timeGraphVisibleWidth = Math.max(1, fTimeGraphScrollPane.getViewportBounds().getWidth());
743 double minDuration = ZOOM_LIMIT * timeGraphVisibleWidth;
744
745 double newDuration = visibleRange.getDuration() * (1.0 / newScaleFactor);
746 newDuration = Math.max(minDuration, newDuration);
747 double durationDelta = newDuration - visibleRange.getDuration();
748 double zoomPivotRatio = (double) (zoomPivot - visibleRange.getStart()) / (double) (visibleRange.getDuration());
749
750 long newStart = visibleRange.getStart() - Math.round(durationDelta * zoomPivotRatio);
751 long newEnd = visibleRange.getEnd() + Math.round(durationDelta - (durationDelta * zoomPivotRatio));
752
753 /* Clamp newStart and newEnd to the full trace's range */
754 TimeRange fullRange = control.getViewContext().getCurrentTraceFullRange();
755 long traceStart = fullRange.getStart();
756 long traceEnd = fullRange.getEnd();
757 newStart = Math.max(newStart, traceStart);
758 newEnd = Math.min(newEnd, traceEnd);
759
760 control.updateVisibleTimeRange(TimeRange.of(newStart, newEnd), true);
761 }
762
763 }
764
765 /**
766 * Get the viewer's zoom actions
767 *
768 * @return The zoom actions
769 */
770 public ZoomActions getZoomActions() {
771 return fZoomActions;
772 }
773
774 // ------------------------------------------------------------------------
775 // Common utils
776 // ------------------------------------------------------------------------
777
778 /**
779 * Determine the timestamps currently represented by the left and right
780 * edges of the time graph pane. In other words, the current "visible range"
781 * the view is showing.
782 *
783 * Note that this method gets its information from UI objects only, so there
784 * might be discrepancies between this and the results of
785 * {@link TimeGraphModelControl#getVisibleTimeRange()}.
786 *
787 * @param newHValue
788 * The "hvalue" property of the horizontal scrollbar to use. If
789 * null, the current value will be retrieved from the scenegraph
790 * object. For example, a scrolling listener might want to pass
791 * its newValue here, since the scenegraph object will not have
792 * been updated yet.
793 * @return The corresponding time range
794 */
795 TimeRange getTimeGraphEdgeTimestamps(@Nullable Double newHValue) {
796 double hvalue = (newHValue == null ? fTimeGraphScrollPane.getHvalue() : newHValue.doubleValue());
797
798 /*
799 * Determine the X positions represented by the edges.
800 */
801 double hmin = fTimeGraphScrollPane.getHmin();
802 double hmax = fTimeGraphScrollPane.getHmax();
803 double contentWidth = fTimeGraphPane.getLayoutBounds().getWidth();
804 double viewportWidth = fTimeGraphScrollPane.getViewportBounds().getWidth();
805 double hoffset = Math.max(0, contentWidth - viewportWidth) * (hvalue - hmin) / (hmax - hmin);
806
807 /*
808 * Convert the positions of the left and right edges to timestamps.
809 */
810 long tsStart = paneXPosToTimestamp(hoffset);
811 long tsEnd = paneXPosToTimestamp(hoffset + viewportWidth);
812
813 return TimeRange.of(tsStart, tsEnd);
814 }
815
816 public double timestampToPaneXPos(long timestamp) {
817 TimeRange fullTimeGraphRange = getViewContext().getCurrentTraceFullRange();
818 return timestampToPaneXPos(timestamp, fullTimeGraphRange, fNanosPerPixel.get());
819 }
820
821 @VisibleForTesting
822 static double timestampToPaneXPos(long timestamp, TimeRange fullTimeGraphRange, double nanosPerPixel) {
823 long start = fullTimeGraphRange.getStart();
824 long end = fullTimeGraphRange.getEnd();
825
826 if (timestamp < start) {
827 throw new IllegalArgumentException(timestamp + " is smaller than trace start time " + start); //$NON-NLS-1$
828 }
829 if (timestamp > end) {
830 throw new IllegalArgumentException(timestamp + " is greater than trace end time " + end); //$NON-NLS-1$
831 }
832
833 double traceDuration = fullTimeGraphRange.getDuration();
834 double timeStampRatio = (timestamp - start) / traceDuration;
835
836 long fullTraceWidthInPixels = (long) (traceDuration / nanosPerPixel);
837 double xPos = fullTraceWidthInPixels * timeStampRatio;
838 return Math.round(xPos);
839 }
840
841 public long paneXPosToTimestamp(double x) {
842 long fullTimeGraphStartTime = getViewContext().getCurrentTraceFullRange().getStart();
843 return paneXPosToTimestamp(x, fTimeGraphPane.getWidth(), fullTimeGraphStartTime, fNanosPerPixel.get());
844 }
845
846 @VisibleForTesting
847 static long paneXPosToTimestamp(double x, double totalWidth, long startTimestamp, double nanosPerPixel) {
848 if (x < 0.0 || totalWidth < 1.0 || x > totalWidth) {
849 throw new IllegalArgumentException("Invalid position arguments: pos=" + x + ", width=" + totalWidth);
850 }
851
852 long ts = Math.round(x * nanosPerPixel);
853 return ts + startTimestamp;
854 }
855
856 /**
857 * Get the current vertical position of the timegraph.
858 *
859 * @return The corresponding VerticalPosition
860 */
861 VerticalPosition getCurrentVerticalPosition() {
862 double vvalue = fTimeGraphScrollPane.getVvalue();
863
864 /* Get the Y position of the top/bottom edges of the pane */
865 double vmin = fTimeGraphScrollPane.getVmin();
866 double vmax = fTimeGraphScrollPane.getVmax();
867 double contentHeight = fTimeGraphPane.getLayoutBounds().getHeight();
868 double viewportHeight = fTimeGraphScrollPane.getViewportBounds().getHeight();
869
870 double vtop = Math.max(0, contentHeight - viewportHeight) * (vvalue - vmin) / (vmax - vmin);
871 double vbottom = vtop + viewportHeight;
872
873 return new VerticalPosition(vtop, vbottom);
874 }
875
876 public static int paneYPosToEntryListIndex(double yPos, double entryHeight) {
877 if (yPos < 0.0 || entryHeight < 0.0) {
878 throw new IllegalArgumentException();
879 }
880
881 return (int) (yPos / entryHeight);
882 }
883
884 // ------------------------------------------------------------------------
885 // Test accessors
886 // ------------------------------------------------------------------------
887
888 private volatile @Nullable CountDownLatch fRepaintLatch = null;
889
890 @VisibleForTesting
891 void prepareWaitForRepaint() {
892 if (fRepaintLatch != null) {
893 throw new IllegalStateException("Do not call this method concurrently!"); //$NON-NLS-1$
894 }
895 fRepaintLatch = new CountDownLatch(1);
896 }
897
898 @VisibleForTesting
899 boolean waitForRepaint() {
900 CountDownLatch latch = fRepaintLatch;
901 boolean done = false;
902 if (latch == null) {
903 throw new IllegalStateException("Do not call this method concurrently!"); //$NON-NLS-1$
904 }
905 try {
906 done = latch.await(100, TimeUnit.MILLISECONDS);
907 } catch (InterruptedException e) {
908 }
909 if (done) {
910 fRepaintLatch = null;
911 }
912 return done;
913 }
914
915 /**
916 * Bypass the redraw thread and do a manual redraw of the current location.
917 */
918 @VisibleForTesting
919 void paintCurrentLocation() {
920 TimeRange currentHorizontalPos = getViewContext().getCurrentVisibleTimeRange();
921 VerticalPosition currentVerticalPos = getCurrentVerticalPosition();
922 paintArea(currentHorizontalPos, currentVerticalPos, true, true, 0);
923 }
924
925 // could eventually be exposed to the user, as "advanced preferences"
926 public DebugOptions getDebugOptions() {
927 return fDebugOptions;
928 }
929
930 public double getCurrentNanosPerPixel() {
931 return fNanosPerPixel.get();
932 }
933
934 public Pane getTimeGraphPane() {
935 return fTimeGraphPane;
936 }
937
938 @VisibleForTesting
939 ScrollPane getTimeGraphScrollPane() {
940 return fTimeGraphScrollPane;
941 }
942
943 @VisibleForTesting
944 TimeGraphArrowLayer getArrowLayer() {
945 return fArrowLayer;
946 }
947
948 @VisibleForTesting
949 TimeGraphDrawnEventLayer getDrawnEventLayer() {
950 return fDrawnEventLayer;
951 }
952 }
This page took 0.06425 seconds and 6 git commands to generate.