1 /*******************************************************************************
2 * Copyright (c) 2016 EfficiOS Inc., Alexandre Montplaisir
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 *******************************************************************************/
10 package org
.lttng
.scope
.tmf2
.views
.ui
.timeline
.widgets
.timegraph
;
12 import static java
.util
.Objects
.requireNonNull
;
14 import java
.util
.Collection
;
15 import java
.util
.Timer
;
16 import java
.util
.concurrent
.CountDownLatch
;
17 import java
.util
.concurrent
.TimeUnit
;
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
;
37 import com
.google
.common
.annotations
.VisibleForTesting
;
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
;
60 * Viewer for the {@link TimelineView}, encapsulating all the view's
63 * Both ScrolledPanes's vertical scrollbars are bound together, so that they
66 * @author Alexandre Montplaisir
68 public class TimeGraphWidget
extends TimeGraphModelView
implements ITimelineWidget
{
70 // ------------------------------------------------------------------------
72 // (Could eventually be moved to separate .css file?)
73 // ------------------------------------------------------------------------
75 public static final Color BACKGROUD_LINES_COLOR
= requireNonNull(Color
.LIGHTBLUE
);
77 private static final String BACKGROUND_STYLE
= "-fx-background-color: rgba(255, 255, 255, 255);"; //$NON-NLS-1$
79 private static final int LABEL_SIDE_MARGIN
= 10;
82 * Height of individual entries (text + states), including padding.
84 * TODO Make this configurable (vertical zoom feature)
86 public static final double ENTRY_HEIGHT
= 20;
88 /** Minimum allowed zoom level, in nanos per pixel */
89 private static final double ZOOM_LIMIT
= 1.0;
91 // ------------------------------------------------------------------------
93 // ------------------------------------------------------------------------
95 private final DebugOptions fDebugOptions
= new DebugOptions();
97 private final ScrollingContext fScrollingCtx
= new ScrollingContext();
98 private final ZoomActions fZoomActions
= new ZoomActions();
101 * Children of the time graph pane are split into groups, so we can easily
102 * redraw or add only some of them.
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
;
112 private final LatestTaskExecutor fTaskExecutor
= new LatestTaskExecutor();
114 private final NestingBoolean fHScrollListenerStatus
;
116 private final BorderPane fBasePane
;
117 private final ToolBar fToolBar
;
118 private final SplitPane fSplitPane
;
120 private final TimeGraphWidgetTreeArea fTreeArea
;
122 private final Pane fTimeGraphPane
;
123 private final ScrollPane fTimeGraphScrollPane
;
125 private final LoadingOverlay fTimeGraphLoadingOverlay
;
127 private final Timer fUiUpdateTimer
= new Timer();
128 private final PeriodicRedrawTask fUiUpdateTimerTask
= new PeriodicRedrawTask(this);
130 private volatile TimeGraphTreeRender fLatestTreeRender
= TimeGraphTreeRender
.EMPTY_RENDER
;
132 /** Current zoom level */
133 private final DoubleProperty fNanosPerPixel
= new SimpleDoubleProperty(1.0);
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.
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
152 public TimeGraphWidget(TimeGraphModelControl control
, NestingBoolean hScrollListenerStatus
) {
154 fHScrollListenerStatus
= hScrollListenerStatus
;
156 // --------------------------------------------------------------------
157 // Prepare the tree part's scene graph
158 // --------------------------------------------------------------------
160 fTreeArea
= new TimeGraphWidgetTreeArea(ENTRY_HEIGHT
, getControl().getModelRenderProvider().traceProperty());
162 // --------------------------------------------------------------------
163 // Prepare the time graph's part scene graph
164 // --------------------------------------------------------------------
166 fTimeGraphLoadingOverlay
= new LoadingOverlay(fDebugOptions
);
167 fTimeGraphLoadingOverlayGroup
= new Group(fTimeGraphLoadingOverlay
);
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());
177 * The order of the layers is important here, it will go from back to
180 fTimeGraphPane
.getChildren().addAll(fBackgroundLayer
.getParentGroup(),
181 fStateLayer
.getParentGroup(),
182 fStateLayer
.getLabelGroup(),
183 fArrowLayer
.getParentGroup(),
184 fDrawnEventLayer
.getParentGroup(),
185 fSelectionLayer
.getParentGroup(),
186 fTimeGraphLoadingOverlayGroup
);
188 fTimeGraphPane
.setStyle(BACKGROUND_STYLE
);
191 * We control the width of the time graph pane programmatically, so
192 * ensure that calls to setPrefWidth set the actual width right away.
194 fTimeGraphPane
.minWidthProperty().bind(fTimeGraphPane
.prefWidthProperty());
195 fTimeGraphPane
.maxWidthProperty().bind(fTimeGraphPane
.prefWidthProperty());
198 * Ensure the time graph pane is always exactly the same vertical size
199 * as the tree pane, so they remain aligned.
202 fTimeGraphPane
.minHeightProperty().bind(fTreeArea
.currentHeightProperty());
203 fTimeGraphPane
.prefHeightProperty().bind(fTreeArea
.currentHeightProperty());
204 fTimeGraphPane
.maxHeightProperty().bind(fTreeArea
.currentHeightProperty());
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.
211 Rectangle clipRect
= new Rectangle();
214 clipRect
.widthProperty().bind(fTimeGraphPane
.widthProperty());
215 clipRect
.heightProperty().bind(fTimeGraphPane
.heightProperty());
216 fTimeGraphPane
.setClip(clipRect
);
219 * Set the loading overlay's size to always follow the size of the pane.
221 fTimeGraphLoadingOverlay
.widthProperty().bind(fTimeGraphPane
.widthProperty());
222 fTimeGraphLoadingOverlay
.heightProperty().bind(fTimeGraphPane
.heightProperty());
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);
232 * Attach the scrollbar listener
234 * TODO Move this to the timeline ?
236 fTimeGraphScrollPane
.hvalueProperty().addListener(fScrollingCtx
.fHScrollChangeListener
);
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
243 fTimeGraphPane
.setOnScroll(fMouseScrollListener
);
246 * Upon reception of any mouse/keyboard event, if there's still a drawn
247 * tooltip it should be hidden.
249 fTimeGraphPane
.addEventFilter(InputEvent
.ANY
, e
-> {
250 StateRectangle selectedState
= fSelectedState
;
251 if (selectedState
!= null) {
252 selectedState
.hideTooltip();
254 /* We must not consume the event here */
257 /* Synchronize the two scrollpanes' vertical scroll bars together */
258 fTreeArea
.getVerticalScrollBar().valueProperty().bindBidirectional(fTimeGraphScrollPane
.vvalueProperty());
260 // --------------------------------------------------------------------
261 // Prepare the top-level area
262 // --------------------------------------------------------------------
264 fToolBar
= new ViewerToolBar(this);
266 fSplitPane
= new SplitPane(fTreeArea
, fTimeGraphScrollPane
);
267 fSplitPane
.setOrientation(Orientation
.HORIZONTAL
);
269 fBasePane
= new BorderPane();
270 fBasePane
.setCenter(fSplitPane
);
271 fBasePane
.setTop(fToolBar
);
273 /* Start the periodic redraw thread */
274 long delay
= fDebugOptions
.uiUpdateDelay
.get();
275 fUiUpdateTimer
.schedule(fUiUpdateTimerTask
, delay
, delay
);
278 public TimeGraphTreeRender
getLatestTreeRender() {
279 return fLatestTreeRender
;
282 // ------------------------------------------------------------------------
284 // ------------------------------------------------------------------------
287 public String
getName() {
288 return getControl().getModelRenderProvider().getName();
292 public Parent
getRootNode() {
297 public @NonNull SplitPane
getSplitPane() {
302 public @NonNull ScrollPane
getTimeBasedScrollPane() {
303 return fTimeGraphScrollPane
;
307 public @Nullable Rectangle
getSelectionRectangle() {
308 return fSelectionLayer
.getSelectionRectangle();
312 public @Nullable Rectangle
getOngoingSelectionRectangle() {
313 return fSelectionLayer
.getOngoingSelectionRectangle();
316 // ------------------------------------------------------------------------
318 // ------------------------------------------------------------------------
321 public void disposeImpl() {
322 /* Stop/cleanup the redraw thread */
323 fUiUpdateTimer
.cancel();
324 fUiUpdateTimer
.purge();
328 public void clear() {
329 Platform
.runLater(() -> {
331 * Clear the generated children of the various groups so they go
332 * back to their initial (post-constructor) state.
336 fBackgroundLayer
.clear();
339 fDrawnEventLayer
.clear();
341 /* Also clear whatever cached objects the viewer currently has. */
342 fLatestTreeRender
= TimeGraphTreeRender
.EMPTY_RENDER
;
343 fUiUpdateTimerTask
.forceRedraw();
348 public void seekVisibleRange(TimeRange newVisibleRange
) {
349 final TimeRange fullTimeGraphRange
= getViewContext().getCurrentTraceFullRange();
351 /* Update the zoom level */
352 long windowTimeRange
= newVisibleRange
.getDuration();
353 double timeGraphVisibleWidth
= fTimeGraphScrollPane
.getViewportBounds().getWidth();
354 if (timeGraphVisibleWidth
< 100) {
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.
360 timeGraphVisibleWidth
= 2000;
362 fNanosPerPixel
.set(windowTimeRange
/ timeGraphVisibleWidth
);
364 double oldTotalWidth
= fTimeGraphPane
.getLayoutBounds().getWidth();
365 double newTotalWidth
= timestampToPaneXPos(fullTimeGraphRange
.getEnd()) - timestampToPaneXPos(fullTimeGraphRange
.getStart());
366 if (newTotalWidth
< 1.0) {
372 if (newVisibleRange
.getStart() == fullTimeGraphRange
.getStart()) {
373 newValue
= fTimeGraphScrollPane
.getHmin();
374 } else if (newVisibleRange
.getEnd() == fullTimeGraphRange
.getEnd()) {
375 newValue
= fTimeGraphScrollPane
.getHmax();
378 * The "hvalue" is in reference to the beginning of the pane, not
379 * the middle point as one could think.
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
387 * See http://stackoverflow.com/a/23518314/4227853 for a great
390 double startPos
= timestampToPaneXPos(newVisibleRange
.getStart());
391 newValue
= startPos
/ (newTotalWidth
- timeGraphVisibleWidth
);
394 fHScrollListenerStatus
.disable();
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.
402 if (Math
.abs(newTotalWidth
- oldTotalWidth
) > 0.5) {
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
);
411 /* Reposition the text labels (don't stretch them!) */
412 fStateLayer
.getRenderedStateLabels().forEach(text
-> {
413 text
.setX(text
.getX() * factor
);
416 /* Reposition the arrows */
417 fArrowLayer
.getRenderedArrows().forEach(arrow
-> {
418 arrow
.setStartX(arrow
.getStartX() * factor
);
419 arrow
.setEndX(arrow
.getEndX() * factor
);
422 /* Reposition the drawn events */
423 fDrawnEventLayer
.getRenderedEvents().forEach(event
-> {
425 * Drawn events use the "translate" properties to define
428 event
.setTranslateX(event
.getTranslateX() * factor
);
433 * Resize the pane itself. Remember min/max are bound to the
434 * "pref" width, so this will change the actual size right away.
436 fTimeGraphPane
.setPrefWidth(newTotalWidth
);
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.
443 fTimeGraphScrollPane
.layout();
446 fTimeGraphScrollPane
.setHvalue(newValue
);
449 fHScrollListenerStatus
.enable();
453 * Redraw the current selection, as it may have moved if we changed the
461 * Paint the specified view area.
464 * The horizontal position where the visible window currently is
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".
474 * The sequence number of this task, used for logging only
476 void paintArea(TimeRange windowRange
, VerticalPosition verticalPos
,
477 boolean movedHorizontally
, boolean movedVertically
,
479 final TimeRange fullTimeGraphRange
= getViewContext().getCurrentTraceFullRange();
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
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
);
492 * Start a new repaint, display the "loading" overlay. The next
493 * paint task to finish will put it back to non-visible.
495 if (getDebugOptions().isLoadingOverlayEnabled
.get()) {
496 fTimeGraphLoadingOverlay
.fadeIn();
499 Task
<@Nullable Void
> task
= new Task
<@Nullable Void
>() {
501 protected @Nullable Void
call() {
502 System
.err
.println("Starting paint task #" + taskSeqNb
);
504 ITimeGraphModelProvider modelProvider
= getControl().getModelRenderProvider();
505 TimeGraphTreeRender treeRender
= modelProvider
.getTreeRender();
511 /* Prepare the tree part, if needed */
512 if (!treeRender
.equals(fLatestTreeRender
)) {
513 fLatestTreeRender
= treeRender
;
514 fTreeArea
.updateTreeContents(treeRender
);
521 /* Paint the background. It's very quick so we can do it every time. */
522 fBackgroundLayer
.drawContents(treeRender
, renderingRange
, verticalPos
, this);
525 * The state rectangles should be redrawn as soon as we move,
526 * either horizontally or vertically.
528 fStateLayer
.setWindowRange(windowRange
);
529 fStateLayer
.drawContents(treeRender
, renderingRange
, verticalPos
, this);
536 * Arrows and drawn events are drawn for the full vertical
537 * range. Only refetch/repaint them if we moved horizontally.
539 if (movedHorizontally
) {
540 fArrowLayer
.drawContents(treeRender
, renderingRange
, verticalPos
, this);
541 fDrawnEventLayer
.drawContents(treeRender
, renderingRange
, verticalPos
, this);
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();
561 System
.err
.println("Queueing task #" + taskSeqNb
);
564 * Attach a listener to the task to receive exceptions thrown within the
567 task
.exceptionProperty().addListener((obs
, oldVal
, newVal
) -> {
568 if (newVal
!= null) {
569 newVal
.printStackTrace();
573 fTaskExecutor
.schedule(task
);
577 public void drawSelection(TimeRange selectionRange
) {
578 fSelectionLayer
.drawSelection(selectionRange
);
581 private void redrawSelection() {
582 TimeRange selectionRange
= getViewContext().getCurrentSelectionTimeRange();
583 drawSelection(selectionRange
);
586 private @Nullable StateRectangle fSelectedState
= null;
589 * Set the selected state rectangle
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
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);
607 state
.setSelected(true);
608 fSelectedState
= state
;
612 * Get the currently selected state interval
614 * @return The current selected state
616 public @Nullable StateRectangle
getSelectedState() {
617 return fSelectedState
;
621 * Return all state rectangles currently present in the timegraph.
623 * @return The rendered state rectangles
625 public Collection
<StateRectangle
> getRenderedStateRectangles() {
626 return fStateLayer
.getRenderedStateRectangles();
629 // ------------------------------------------------------------------------
630 // Mouse event listeners
631 // ------------------------------------------------------------------------
634 * Class encapsulating the scrolling operations of the time graph pane.
636 * The mouse entered/exited handlers ensure only the scrollpane being
637 * interacted by the user is the one sending the synchronization signals.
639 private class ScrollingContext
{
642 * Listener for the horizontal scrollbar changes
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");
649 if (!fHScrollListenerStatus
.enabledProperty().get()) {
650 System
.out
.println("HScroll listener triggered but inactive");
654 System
.out
.println("HScroll change listener triggered, oldval=" + oldValue
.toString() + ", newval=" + newValue
.toString());
656 /* We need to specify the new value here, or else the old one will be used */
657 TimeRange range
= getTimeGraphEdgeTimestamps(newValue
.doubleValue());
659 System
.out
.println("Sending visible range update: " + range
.toString());
661 getControl().updateVisibleTimeRange(range
, false);
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.
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.
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).
680 private final EventHandler
<ScrollEvent
> fMouseScrollListener
= e
-> {
681 boolean forceUseMousePosition
= false;
683 if (!e
.isControlDown()) {
687 if (e
.isShiftDown()) {
688 forceUseMousePosition
= true;
692 double delta
= e
.getDeltaY();
693 boolean zoomIn
= (delta
> 0.0); // false means a zoom-out
696 * getX() corresponds to the X position of the mouse on the time graph.
697 * This is seriously awesome.
699 fZoomActions
.zoom(zoomIn
, forceUseMousePosition
, e
.getX());
703 // ------------------------------------------------------------------------
704 // View-specific actions
705 // These do not come from the control, but from the view itself
706 // ------------------------------------------------------------------------
709 * Utils class encapsulating zoom operations
711 public class ZoomActions
{
713 public void zoom(boolean zoomIn
, boolean forceUseMousePosition
, @Nullable Double mouseX
) {
714 final double zoomStep
= fDebugOptions
.zoomStep
.get();
716 double newScaleFactor
= (zoomIn ?
1.0 * (1 + zoomStep
) : 1.0 * (1 / (1 + zoomStep
)));
718 /* Send a corresponding window-range signal to the control */
719 TimeGraphModelControl control
= getControl();
720 TimeRange visibleRange
= getViewContext().getCurrentVisibleTimeRange();
722 TimeRange currentSelection
= getViewContext().getCurrentSelectionTimeRange();
723 long currentSelectionCenter
= ((currentSelection
.getDuration() / 2) + currentSelection
.getStart());
724 boolean currentSelectionCenterIsVisible
= visibleRange
.contains(currentSelectionCenter
);
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
);
737 /* Pivot on center of visible range */
738 zoomPivot
= visibleRange
.getStart() + (visibleRange
.getDuration() / 2);
741 /* Prevent going closer than the zoom limit */
742 double timeGraphVisibleWidth
= Math
.max(1, fTimeGraphScrollPane
.getViewportBounds().getWidth());
743 double minDuration
= ZOOM_LIMIT
* timeGraphVisibleWidth
;
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());
750 long newStart
= visibleRange
.getStart() - Math
.round(durationDelta
* zoomPivotRatio
);
751 long newEnd
= visibleRange
.getEnd() + Math
.round(durationDelta
- (durationDelta
* zoomPivotRatio
));
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
);
760 control
.updateVisibleTimeRange(TimeRange
.of(newStart
, newEnd
), true);
766 * Get the viewer's zoom actions
768 * @return The zoom actions
770 public ZoomActions
getZoomActions() {
774 // ------------------------------------------------------------------------
776 // ------------------------------------------------------------------------
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.
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()}.
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
793 * @return The corresponding time range
795 TimeRange
getTimeGraphEdgeTimestamps(@Nullable Double newHValue
) {
796 double hvalue
= (newHValue
== null ? fTimeGraphScrollPane
.getHvalue() : newHValue
.doubleValue());
799 * Determine the X positions represented by the edges.
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
);
808 * Convert the positions of the left and right edges to timestamps.
810 long tsStart
= paneXPosToTimestamp(hoffset
);
811 long tsEnd
= paneXPosToTimestamp(hoffset
+ viewportWidth
);
813 return TimeRange
.of(tsStart
, tsEnd
);
816 public double timestampToPaneXPos(long timestamp
) {
817 TimeRange fullTimeGraphRange
= getViewContext().getCurrentTraceFullRange();
818 return timestampToPaneXPos(timestamp
, fullTimeGraphRange
, fNanosPerPixel
.get());
822 static double timestampToPaneXPos(long timestamp
, TimeRange fullTimeGraphRange
, double nanosPerPixel
) {
823 long start
= fullTimeGraphRange
.getStart();
824 long end
= fullTimeGraphRange
.getEnd();
826 if (timestamp
< start
) {
827 throw new IllegalArgumentException(timestamp
+ " is smaller than trace start time " + start
); //$NON-NLS-1$
829 if (timestamp
> end
) {
830 throw new IllegalArgumentException(timestamp
+ " is greater than trace end time " + end
); //$NON-NLS-1$
833 double traceDuration
= fullTimeGraphRange
.getDuration();
834 double timeStampRatio
= (timestamp
- start
) / traceDuration
;
836 long fullTraceWidthInPixels
= (long) (traceDuration
/ nanosPerPixel
);
837 double xPos
= fullTraceWidthInPixels
* timeStampRatio
;
838 return Math
.round(xPos
);
841 public long paneXPosToTimestamp(double x
) {
842 long fullTimeGraphStartTime
= getViewContext().getCurrentTraceFullRange().getStart();
843 return paneXPosToTimestamp(x
, fTimeGraphPane
.getWidth(), fullTimeGraphStartTime
, fNanosPerPixel
.get());
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
);
852 long ts
= Math
.round(x
* nanosPerPixel
);
853 return ts
+ startTimestamp
;
857 * Get the current vertical position of the timegraph.
859 * @return The corresponding VerticalPosition
861 VerticalPosition
getCurrentVerticalPosition() {
862 double vvalue
= fTimeGraphScrollPane
.getVvalue();
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();
870 double vtop
= Math
.max(0, contentHeight
- viewportHeight
) * (vvalue
- vmin
) / (vmax
- vmin
);
871 double vbottom
= vtop
+ viewportHeight
;
873 return new VerticalPosition(vtop
, vbottom
);
876 public static int paneYPosToEntryListIndex(double yPos
, double entryHeight
) {
877 if (yPos
< 0.0 || entryHeight
< 0.0) {
878 throw new IllegalArgumentException();
881 return (int) (yPos
/ entryHeight
);
884 // ------------------------------------------------------------------------
886 // ------------------------------------------------------------------------
888 private volatile @Nullable CountDownLatch fRepaintLatch
= null;
891 void prepareWaitForRepaint() {
892 if (fRepaintLatch
!= null) {
893 throw new IllegalStateException("Do not call this method concurrently!"); //$NON-NLS-1$
895 fRepaintLatch
= new CountDownLatch(1);
899 boolean waitForRepaint() {
900 CountDownLatch latch
= fRepaintLatch
;
901 boolean done
= false;
903 throw new IllegalStateException("Do not call this method concurrently!"); //$NON-NLS-1$
906 done
= latch
.await(100, TimeUnit
.MILLISECONDS
);
907 } catch (InterruptedException e
) {
910 fRepaintLatch
= null;
916 * Bypass the redraw thread and do a manual redraw of the current location.
919 void paintCurrentLocation() {
920 TimeRange currentHorizontalPos
= getViewContext().getCurrentVisibleTimeRange();
921 VerticalPosition currentVerticalPos
= getCurrentVerticalPosition();
922 paintArea(currentHorizontalPos
, currentVerticalPos
, true, true, 0);
925 // could eventually be exposed to the user, as "advanced preferences"
926 public DebugOptions
getDebugOptions() {
927 return fDebugOptions
;
930 public double getCurrentNanosPerPixel() {
931 return fNanosPerPixel
.get();
934 public Pane
getTimeGraphPane() {
935 return fTimeGraphPane
;
939 ScrollPane
getTimeGraphScrollPane() {
940 return fTimeGraphScrollPane
;
944 TimeGraphArrowLayer
getArrowLayer() {
949 TimeGraphDrawnEventLayer
getDrawnEventLayer() {
950 return fDrawnEventLayer
;