org.lttng.scope.tmf2.views.ui.timegraph.swtjfx.SwtJfxTimeGraphViewer.java Source code

Java tutorial

Introduction

Here is the source code for org.lttng.scope.tmf2.views.ui.timegraph.swtjfx.SwtJfxTimeGraphViewer.java

Source

/*******************************************************************************
 * Copyright (c) 2016 EfficiOS Inc., Alexandre Montplaisir
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License v1.0 which
 * accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *******************************************************************************/

package org.lttng.scope.tmf2.views.ui.timegraph.swtjfx;

import static java.util.Objects.requireNonNull;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.StringJoiner;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager;
import org.lttng.scope.tmf2.views.core.timegraph.control.TimeGraphModelControl;
import org.lttng.scope.tmf2.views.core.timegraph.model.provider.ITimeGraphModelRenderProvider;
import org.lttng.scope.tmf2.views.core.timegraph.model.render.states.TimeGraphStateRender;
import org.lttng.scope.tmf2.views.core.timegraph.model.render.tree.TimeGraphTreeElement;
import org.lttng.scope.tmf2.views.core.timegraph.model.render.tree.TimeGraphTreeRender;
import org.lttng.scope.tmf2.views.core.timegraph.view.TimeGraphModelView;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;

import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.concurrent.Task;
import javafx.embed.swt.FXCanvas;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeLineCap;

/**
 * Viewer for the {@link SwtJfxTimeGraphView}, encapsulating all the view's
 * controls.
 *
 * Its contents consist of:
 *
 * TODO update this to its final form
 * <pre>
 * SashForm fBaseControl (parent is passed from the view)
 *  + FXCanvas
 *  |   + ScrollPane
 *  |       + TreeView (?), contains the list of threads
 *  + FXCanvas
 *      + ScrollPane, will contain the time graph area
 *          + Pane, gets resized to very large horizontal size to represent the whole trace range
 *             + Canvas, canvas children are tiled on the Pane to show the content of one Render each
 *             + Canvas
 *             +  ...
 * </pre>
 *
 * Both ScrolledPanes's vertical scrollbars are bound together, so that they
 * scroll together.
 *
 * @author Alexandre Montplaisir
 */
public class SwtJfxTimeGraphViewer extends TimeGraphModelView {

    // ------------------------------------------------------------------------
    // Helper classes
    // ------------------------------------------------------------------------

    private static class HorizontalPosition {
        public long fStartTime = 0L;
        public long fEndTime = 0L;
    }

    private static class VerticalPosition {
        public double fTopPos = 0.0;
        public double fBottomPos = 0.0;
        public double fContentHeight = 0.0;
    }

    // ------------------------------------------------------------------------
    // Class fields
    // ------------------------------------------------------------------------

    private static final double MAX_CANVAS_WIDTH = 2000.0;
    private static final double MAX_CANVAS_HEIGHT = 2000.0;

    // ------------------------------------------------------------------------
    // Style definitions
    // (Could eventually be moved to separate .css file?)
    // ------------------------------------------------------------------------

    private static final Color BACKGROUD_LINES_COLOR = requireNonNull(Color.LIGHTBLUE);
    private static final String BACKGROUND_STYLE = "-fx-background-color: rgba(255, 255, 255, 255);"; //$NON-NLS-1$

    private static final double SELECTION_STROKE_WIDTH = 1;
    private static final Color SELECTION_STROKE_COLOR = requireNonNull(Color.BLUE);
    private static final Color SELECTION_FILL_COLOR = requireNonNull(Color.LIGHTBLUE.deriveColor(0, 1.2, 1, 0.4));

    private static final int LABEL_SIDE_MARGIN = 10;

    /**
     * Height of individual entries (text + states), including padding.
     *
     * TODO Make this configurable (vertical zoom feature)
     */
    public static final double ENTRY_HEIGHT = 20;

    /** Number of tree elements to print above *and* below the visible range */
    private static final int ENTRY_PREFETCHING = 5;

    /** Time between UI updates, in milliseconds */
    private static final int UI_UPDATE_DELAY = 250;

    // ------------------------------------------------------------------------
    // Instance fields
    // ------------------------------------------------------------------------

    private final SelectionContext fSelectionCtx = new SelectionContext();
    private final ScrollingContext fScrollingCtx = new ScrollingContext();

    private final LatestTaskExecutor fTaskExecutor = new LatestTaskExecutor();

    private final SashForm fBaseControl;

    private final FXCanvas fTreeFXCanvas;
    private final FXCanvas fTimeGraphFXCanvas;

    private final Pane fTreePane;
    private final ScrollPane fTreeScrollPane;
    private final Pane fTimeGraphPane;
    private final ScrollPane fTimeGraphScrollPane;

    /*
     * Children of the time graph pane are split into groups, so we can easily
     * redraw or add only some of them.
     */
    private final Group fTimeGraphStatesLayer;
    private final Group fTimeGraphSelectionLayer;
    // TODO Layers for markers, arrows

    private final Rectangle fSelectionRect;
    private final Rectangle fOngoingSelectionRect;

    private final VerticalPosition fVerticalPosition = new VerticalPosition();

    private final AtomicLong fTaskSeq = new AtomicLong();
    private final Timer fUiUpdateTimer = new Timer();
    private final TimerTask fUiUpdateTimerTask = new TimerTask() {

        private final HorizontalPosition fPreviousHorizontalPos = new HorizontalPosition();
        private final VerticalPosition fPreviousVerticalPosition = new VerticalPosition();

        @Override
        public void run() {
            long start = getControl().getVisibleTimeRangeStart();
            long end = getControl().getVisibleTimeRangeEnd();
            double topPos = fVerticalPosition.fTopPos;
            double bottomPos = fVerticalPosition.fBottomPos;
            double contentHeight = fVerticalPosition.fContentHeight;

            if (start == fPreviousHorizontalPos.fStartTime && end == fPreviousHorizontalPos.fEndTime
                    && topPos == fPreviousVerticalPosition.fTopPos
                    && bottomPos == fPreviousVerticalPosition.fBottomPos
                    && contentHeight == fPreviousVerticalPosition.fContentHeight) {
                /*
                 * Exact same position as the last one we've seen, no need to
                 * repaint.
                 */
                return;
            }
            fPreviousHorizontalPos.fStartTime = start;
            fPreviousHorizontalPos.fEndTime = end;
            fPreviousVerticalPosition.fTopPos = topPos;
            fPreviousVerticalPosition.fBottomPos = bottomPos;
            fPreviousVerticalPosition.fContentHeight = contentHeight;

            paintArea(start, end, fTaskSeq.getAndIncrement());
        }
    };

    private volatile TimeGraphTreeRender fLatestTreeRender = TimeGraphTreeRender.EMPTY_RENDER;

    /** Current zoom level */
    private double fNanosPerPixel = 1.0;

    /**
     * Constructor
     *
     * @param parent
     *            Parent SWT composite
     */
    public SwtJfxTimeGraphViewer(Composite parent, TimeGraphModelControl control) {
        super(control);

        // TODO Convert this sash to JavaFX too?
        fBaseControl = new SashForm(parent, SWT.NONE);

        fTreeFXCanvas = new FXCanvas(fBaseControl, SWT.NONE);
        fTimeGraphFXCanvas = new FXCanvas(fBaseControl, SWT.NONE);

        // TODO Base on time-alignment
        fBaseControl.setWeights(new int[] { 15, 85 });

        // --------------------------------------------------------------------
        // Prepare the tree part's scene graph
        // --------------------------------------------------------------------

        fTreePane = new Pane();

        fTreeScrollPane = new ScrollPane(fTreePane);
        /* We only show the time graph's vertical scrollbar */
        fTreeScrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
        fTreeScrollPane.setHbarPolicy(ScrollBarPolicy.ALWAYS);

        // --------------------------------------------------------------------
        // Prepare the time graph's part scene graph
        // --------------------------------------------------------------------

        fSelectionRect = new Rectangle();
        fOngoingSelectionRect = new Rectangle();

        Stream.of(fSelectionRect, fOngoingSelectionRect).forEach(rect -> {
            rect.setStroke(SELECTION_STROKE_COLOR);
            rect.setStrokeWidth(SELECTION_STROKE_WIDTH);
            rect.setStrokeLineCap(StrokeLineCap.ROUND);
            rect.setFill(SELECTION_FILL_COLOR);
        });

        fTimeGraphStatesLayer = new Group();
        fTimeGraphSelectionLayer = new Group(fSelectionRect, fOngoingSelectionRect);

        fTimeGraphPane = new Pane(fTimeGraphStatesLayer, fTimeGraphSelectionLayer);
        fTimeGraphPane.setStyle(BACKGROUND_STYLE);
        fTimeGraphPane.addEventHandler(MouseEvent.MOUSE_PRESSED, fSelectionCtx.fMousePressedEventHandler);
        fTimeGraphPane.addEventHandler(MouseEvent.MOUSE_DRAGGED, fSelectionCtx.fMouseDraggedEventHandler);
        fTimeGraphPane.addEventHandler(MouseEvent.MOUSE_RELEASED, fSelectionCtx.fMouseReleasedEventHandler);

        /*
         * We control the width of the time graph pane programatically, so
         * ensure that calls to setPrefWidth set the actual width right away.
         */
        fTimeGraphPane.minWidthProperty().bind(fTimeGraphPane.prefWidthProperty());
        fTimeGraphPane.maxWidthProperty().bind(fTimeGraphPane.prefWidthProperty());

        /*
         * Ensure the time graph pane is always exactly the same vertical size
         * as the tree pane, so they remain aligned.
         */
        fTimeGraphPane.minHeightProperty().bind(fTreePane.heightProperty());
        fTimeGraphPane.prefHeightProperty().bind(fTreePane.heightProperty());
        fTimeGraphPane.maxHeightProperty().bind(fTreePane.heightProperty());

        fTimeGraphScrollPane = new ScrollPane(fTimeGraphPane);
        fTimeGraphScrollPane.setVbarPolicy(ScrollBarPolicy.ALWAYS);
        fTimeGraphScrollPane.setHbarPolicy(ScrollBarPolicy.ALWAYS);

        //        fTimeGraphScrollPane.viewportBoundsProperty().addListener(fScrollingCtx.fHScrollChangeListener);
        fTimeGraphScrollPane.setOnMouseEntered(fScrollingCtx.fMouseEnteredEventHandler);
        fTimeGraphScrollPane.setOnMouseExited(fScrollingCtx.fMouseExitedEventHandler);
        fTimeGraphScrollPane.hvalueProperty().addListener(fScrollingCtx.fHScrollChangeListener);
        fTimeGraphScrollPane.vvalueProperty().addListener(fScrollingCtx.fVScrollChangeListener);

        /* Synchronize the two scrollpanes' vertical scroll bars together */
        fTreeScrollPane.vvalueProperty().bindBidirectional(fTimeGraphScrollPane.vvalueProperty());

        // --------------------------------------------------------------------
        // Hook the parts into the SWT window
        // --------------------------------------------------------------------

        fTreeFXCanvas.setScene(new Scene(fTreeScrollPane));
        fTimeGraphFXCanvas.setScene(new Scene(fTimeGraphScrollPane));

        /*
         * Initially populate the viewer with the context of the current trace.
         */
        ITmfTrace trace = TmfTraceManager.getInstance().getActiveTrace();
        getControl().initializeForTrace(trace);

        fUiUpdateTimer.schedule(fUiUpdateTimerTask, UI_UPDATE_DELAY, UI_UPDATE_DELAY);
    }

    // ------------------------------------------------------------------------
    // Test accessors
    // ------------------------------------------------------------------------

    @VisibleForTesting
    protected Pane getTimeGraphPane() {
        return fTimeGraphPane;
    }

    @VisibleForTesting
    protected ScrollPane getTimeGraphScrollPane() {
        return fTimeGraphScrollPane;
    }

    // ------------------------------------------------------------------------
    // Operations
    // ------------------------------------------------------------------------

    @Override
    public void disposeImpl() {
        fUiUpdateTimer.cancel();
        fUiUpdateTimer.purge();
    }

    @Override
    public void clear() {
        // TODO
    }

    @Override
    public void seekVisibleRange(long visibleWindowStartTime, long visibleWindowEndTime) {
        final long fullTimeGraphStart = getControl().getFullTimeGraphStartTime();
        final long fullTimeGraphEnd = getControl().getFullTimeGraphEndTime();

        /* Update the zoom level */
        long windowTimeRange = visibleWindowEndTime - visibleWindowStartTime;
        double timeGraphWidth = fTimeGraphScrollPane.getWidth();
        fNanosPerPixel = windowTimeRange / timeGraphWidth;

        double timeGraphAreaWidth = timestampToPaneXPos(fullTimeGraphEnd) - timestampToPaneXPos(fullTimeGraphStart);
        if (timeGraphAreaWidth < 1.0) {
            // FIXME
            return;
        }

        double newValue;
        if (visibleWindowStartTime == fullTimeGraphStart) {
            newValue = fTimeGraphScrollPane.getHmin();
        } else if (visibleWindowEndTime == fullTimeGraphEnd) {
            newValue = fTimeGraphScrollPane.getHmax();
        } else {
            // FIXME Not aligned perfectly yet, see how the scrolling
            // listener does it?
            long targetTs = (visibleWindowStartTime + visibleWindowEndTime) / 2;
            double xPos = timestampToPaneXPos(targetTs);
            newValue = xPos / timeGraphAreaWidth;
        }

        fTimeGraphPane.setPrefWidth(timeGraphAreaWidth);
        fTimeGraphScrollPane.setHvalue(newValue);
    }

    private void paintArea(long windowStartTime, long windowEndTime, long taskSeqNb) {
        final long fullTimeGraphStart = getControl().getFullTimeGraphStartTime();
        final long fullTimeGraphEnd = getControl().getFullTimeGraphEndTime();

        /*
         * Get the current target width of the viewer, so we know at which
         * resolution we must do state system queries.
         *
         * Yes! We can query the size of visible components outside of the UI
         * thread! Praise the JavaFX!
         */
        long treePaneWidth = Math.round(fTreeScrollPane.getWidth());

        long windowTimeRange = windowEndTime - windowStartTime;

        /*
         * Request the needed renders and prepare the corresponding
         * canvases. We target at most one "window width" before and
         * after the current window, clamped by the trace's start and
         * end.
         */
        final long renderingStartTime = Math.max(fullTimeGraphStart, windowStartTime - windowTimeRange);
        final long renderingEndTime = Math.min(fullTimeGraphEnd, windowEndTime + windowTimeRange);
        final long renderTimeRange = (long) (MAX_CANVAS_WIDTH * fNanosPerPixel);
        final long resolution = Math.max(1, Math.round(fNanosPerPixel));

        if (renderTimeRange < 1) {
            return;
        }

        final VerticalPosition vertical = fVerticalPosition;

        Task<@Nullable Void> task = new Task<@Nullable Void>() {
            @Override
            protected @Nullable Void call() {

                long start = System.nanoTime();
                System.err.println("Starting paint task #" + taskSeqNb);

                ITimeGraphModelRenderProvider renderProvider = getControl().getModelRenderProvider();
                TimeGraphTreeRender treeRender = renderProvider.getTreeRender();
                final List<TimeGraphTreeElement> allTreeElements = treeRender.getAllTreeElements();

                if (isCancelled()) {
                    System.err.println("task #" + taskSeqNb + " was cancelled before generating the states");
                    return null;
                }

                long afterTreeRender = System.nanoTime();

                final int nbElements = allTreeElements.size();

                int topEntry = Math.max(0,
                        paneYPosToEntryListIndex(vertical.fTopPos, vertical.fContentHeight, nbElements)
                                - ENTRY_PREFETCHING);
                int bottomEntry = Math.min(nbElements,
                        paneYPosToEntryListIndex(vertical.fBottomPos, vertical.fContentHeight, nbElements)
                                + ENTRY_PREFETCHING);

                System.out.println("topEntry=" + topEntry + ", bottomEntry=" + bottomEntry);

                List<TimeGraphStateRender> stateRenders = allTreeElements
                        .subList(topEntry, bottomEntry).stream().map(treeElem -> renderProvider
                                .getStateRender(treeElem, renderingStartTime, renderingEndTime, resolution, this))
                        .collect(Collectors.toList());

                if (isCancelled()) {
                    System.err.println("task #" + taskSeqNb + " was cancelled before generating the contents");
                    return null;
                }

                long afterStateRenders = System.nanoTime();

                /* Prepare the tree part, if needed */
                @Nullable
                Node treeContents;
                if (treeRender.equals(fLatestTreeRender)) {
                    treeContents = null;
                } else {
                    fLatestTreeRender = treeRender;
                    treeContents = prepareTreeContents(treeRender, treePaneWidth);
                }

                /* Prepare the time graph part */
                Node timeGraphContents = prepareTimeGraphContents(stateRenders, topEntry);

                if (isCancelled()) {
                    System.err.println("task #" + taskSeqNb + " was cancelled before updating the view");
                    return null;
                }

                long afterJavaFXObjects = System.nanoTime();

                StringJoiner sj = new StringJoiner(", ", "Repaint breakdown (#" + taskSeqNb + "): ", "")
                        .add("Generating tree render=" + String.format("%,d", afterTreeRender - start) + " ns")
                        .add("Generating state renders= "
                                + String.format("%,d", afterStateRenders - afterTreeRender) + " ns")
                        .add("Generating JavaFX objects="
                                + String.format("%,d", afterJavaFXObjects - afterStateRenders) + " ns");
                System.err.println(sj.toString());

                /* Update the view! */
                // Display.getDefault().syncExec( () -> {
                Platform.runLater(() -> {
                    long startUI = System.nanoTime();
                    if (treeContents != null) {
                        fTreePane.getChildren().clear();
                        fTreePane.getChildren().add(treeContents);
                    }

                    long afterTreeUI = System.nanoTime();

                    fTimeGraphStatesLayer.getChildren().clear();
                    fTimeGraphStatesLayer.getChildren().add(timeGraphContents);

                    long endUI = System.nanoTime();
                    StringJoiner sjui = new StringJoiner(", ", "UI Update (#" + taskSeqNb + "): ", "")
                            .add("Drawing tree=" + String.format("%,d", afterTreeUI - startUI) + " ns")
                            .add("Drawing states= " + String.format("%,d", endUI - afterTreeUI) + " ns");
                    System.err.println(sjui.toString());
                });

                return null;
            }
        };

        System.err.println("Queueing task #" + taskSeqNb);
        fTaskExecutor.schedule(task);
    }

    @Override
    public void drawSelection(long selectionStartTime, long selectionEndTime) {
        double xStart = timestampToPaneXPos(selectionStartTime);
        double xEnd = timestampToPaneXPos(selectionEndTime);
        double xWidth = xEnd - xStart;

        fSelectionRect.setX(xStart);
        fSelectionRect.setY(0);
        fSelectionRect.setWidth(xWidth);
        fSelectionRect.setHeight(fTimeGraphPane.getHeight());

        fSelectionRect.setVisible(true);
    }

    // ------------------------------------------------------------------------
    // Methods related to the Tree area
    // ------------------------------------------------------------------------

    private static Node prepareTreeContents(TimeGraphTreeRender treeRender, double paneWidth) {
        /* Prepare the tree element objects */
        List<Label> treeElements = treeRender.getAllTreeElements().stream()
                // TODO Put as a real tree. TreeView ?
                .map(elem -> new Label(elem.getName())).peek(label -> {
                    label.setPrefHeight(ENTRY_HEIGHT);
                    label.setPadding(new Insets(0, LABEL_SIDE_MARGIN, 0, LABEL_SIDE_MARGIN));
                    /*
                     * Re-set the solid background for the labels, so we do not
                     * see the background lines through.
                     */
                    label.setStyle(BACKGROUND_STYLE);
                }).collect(Collectors.toList());

        VBox treeElemsBox = new VBox(); // Change to TreeView eventually ?
        treeElemsBox.getChildren().addAll(treeElements);

        /* Prepare the Canvases with the horizontal alignment lines */
        List<Canvas> canvases = new ArrayList<>();
        int maxEntriesPerCanvas = (int) (MAX_CANVAS_HEIGHT / ENTRY_HEIGHT);
        Lists.partition(treeElements, maxEntriesPerCanvas).forEach(subList -> {
            int nbElements = subList.size();
            double height = nbElements * ENTRY_HEIGHT;

            Canvas canvas = new Canvas(paneWidth, height);
            drawBackgroundLines(canvas, ENTRY_HEIGHT);
            canvas.setCache(true);
            canvases.add(canvas);
        });
        VBox canvasBox = new VBox();
        canvasBox.getChildren().addAll(canvases);

        /* Put the background Canvas and the Tree View into their containers */
        StackPane stackPane = new StackPane(canvasBox, treeElemsBox);
        stackPane.setStyle(BACKGROUND_STYLE);
        return stackPane;
    }

    // ------------------------------------------------------------------------
    // Methods related to the Time Graph area
    // ------------------------------------------------------------------------

    private Node prepareTimeGraphContents(List<TimeGraphStateRender> stateRenders, int topEntry) {
        Collection<Node> rectangles = IntStream.range(0, stateRenders.size()).parallel()
                .mapToObj(idx -> getRectanglesForStateRender(stateRenders.get(idx), idx + topEntry))
                .flatMap(Function.identity()).collect(Collectors.toSet());

        return new Group(rectangles);
    }

    /**
     * Get the vertically-tiled Canvas's for a single render. They will
     * be already relocated correctly, so the collection's order does not
     * matter.
     *
     * @param render
     *            The render
     * @return The vertical set of canvases
     */
    private Stream<StateRectangle> getRectanglesForStateRender(TimeGraphStateRender stateRender, int entryIndex) {
        return stateRender.getStateIntervals().stream()
                .map(interval -> new StateRectangle(this, interval, entryIndex));
    }

    // ------------------------------------------------------------------------
    // Mouse event listeners
    // ------------------------------------------------------------------------

    /**
     * Class encapsulating the time range selection, related drawing and
     * listeners.
     */
    private class SelectionContext {

        private boolean fOngoingSelection;
        private double fMouseOriginX;

        public final EventHandler<MouseEvent> fMousePressedEventHandler = e -> {
            if (e.isShiftDown() || e.isControlDown() || e.isSecondaryButtonDown() || e.isMiddleButtonDown()) {
                /* Do other things! */
                // TODO!
                return;
            }

            if (fOngoingSelection) {
                return;
            }

            /* Remove the current selection, if there is one */
            fSelectionRect.setVisible(false);

            fMouseOriginX = e.getX();

            fOngoingSelectionRect.setX(fMouseOriginX);
            fOngoingSelectionRect.setY(0);
            fOngoingSelectionRect.setWidth(0);
            fOngoingSelectionRect.setHeight(fTimeGraphPane.getHeight());

            fOngoingSelectionRect.setVisible(true);

            e.consume();

            fOngoingSelection = true;
        };

        public final EventHandler<MouseEvent> fMouseDraggedEventHandler = e -> {
            double newX = e.getX();
            double offsetX = newX - fMouseOriginX;

            if (offsetX > 0) {
                fOngoingSelectionRect.setX(fMouseOriginX);
                fOngoingSelectionRect.setWidth(offsetX);
            } else {
                fOngoingSelectionRect.setX(newX);
                fOngoingSelectionRect.setWidth(-offsetX);
            }

            e.consume();
        };

        public final EventHandler<MouseEvent> fMouseReleasedEventHandler = e -> {
            fOngoingSelectionRect.setVisible(false);

            e.consume();

            /* Send a time range selection signal for the currently selected time range */
            double startX = Math.max(0, fOngoingSelectionRect.getX());
            // FIXME Possible glitch when selecting backwards outside of the window
            double endX = Math.min(fTimeGraphPane.getWidth(), startX + fOngoingSelectionRect.getWidth());
            long tsStart = paneXPosToTimestamp(startX);
            long tsEnd = paneXPosToTimestamp(endX);

            getControl().updateTimeRangeSelection(tsStart, tsEnd);

            fOngoingSelection = false;
        };
    }

    /**
     * Class encapsulating the scrolling operations of the time graph pane.
     *
     * The mouse entered/exited handlers ensure only the scrollpane being
     * interacted by the user is the one sending the synchronization signals.
     */
    private class ScrollingContext {

        private boolean fUserActionOngoing = false;

        private final EventHandler<MouseEvent> fMouseEnteredEventHandler = e -> {
            fUserActionOngoing = true;
        };

        private final EventHandler<MouseEvent> fMouseExitedEventHandler = e -> {
            fUserActionOngoing = false;
        };

        /**
         * Listener for the horizontal scrollbar changes
         */
        private final ChangeListener<Number> fHScrollChangeListener = (observable, oldValue, newValue) -> {
            if (!fUserActionOngoing) {
                System.out.println("HScroll listener triggered but inactive");
                return;
            }

            System.out.println("HScroll change listener triggered, oldval=" + oldValue.toString() + ", newval="
                    + newValue.toString());

            /*
             * Determine the X position represented by the left edge of the pane
             */
            double hmin = fTimeGraphScrollPane.getHmin();
            double hmax = fTimeGraphScrollPane.getHmax();
            /* scrollPane.getHvalue() would return the *old* value here! */
            double hvalue = newValue.doubleValue();
            double contentWidth = fTimeGraphPane.getLayoutBounds().getWidth();
            double viewportWidth = fTimeGraphScrollPane.getViewportBounds().getWidth();
            double hoffset = Math.max(0, contentWidth - viewportWidth) * (hvalue - hmin) / (hmax - hmin);

            /*
             * Convert the positions of the left and right edges to timestamps,
             * and send a window range update signal
             */
            long tsStart = paneXPosToTimestamp(hoffset);
            long tsEnd = paneXPosToTimestamp(hoffset + viewportWidth);

            System.out.printf("Offset: %.1f, width: %.1f %n", hoffset, viewportWidth);
            System.out.printf("Sending visible range update: %,d to %,d%n", tsStart, tsEnd);

            getControl().updateVisibleTimeRange(tsStart, tsEnd);

            /*
             * The control will not send thisn signal back to us (to avoid
             * jitter while scrolling), but the next UI update should refresh
             * the view accordingly.
             */
        };

        private final ChangeListener<Number> fVScrollChangeListener = (observable, oldValue, newValue) -> {
            if (!fUserActionOngoing) {
                System.out.println("HScroll listener triggered but inactive");
                return;
            }

            /* Get the Y position of the top/bottom edges of the pane */
            double vmin = fTreeScrollPane.getVmin();
            double vmax = fTreeScrollPane.getVmax();
            double vvalue = newValue.doubleValue();
            double contentHeight = fTreePane.getLayoutBounds().getHeight();
            double viewportHeight = fTreeScrollPane.getViewportBounds().getHeight();

            double vtop = Math.max(0, contentHeight - viewportHeight) * (vvalue - vmin) / (vmax - vmin);
            double vbottom = vtop + viewportHeight;

            fVerticalPosition.fTopPos = vtop;
            fVerticalPosition.fBottomPos = vbottom;
            fVerticalPosition.fContentHeight = contentHeight;

            /* Next UI update will take these coordinates in consideration */
        };
    }

    // ------------------------------------------------------------------------
    // Common utils
    // ------------------------------------------------------------------------

    private static void drawBackgroundLines(Canvas canvas, double entryHeight) {
        double width = canvas.getWidth();
        int nbLines = (int) (canvas.getHeight() / entryHeight);

        GraphicsContext gc = canvas.getGraphicsContext2D();
        gc.save();

        gc.setStroke(BACKGROUD_LINES_COLOR);
        gc.setLineWidth(1);
        /* average+2 gives the best-looking output */
        DoubleStream.iterate((ENTRY_HEIGHT / 2) + 2, i -> i + entryHeight).limit(nbLines).forEach(yPos -> {
            gc.strokeLine(0, yPos, width, yPos);
        });

        gc.restore();
    }

    double timestampToPaneXPos(long timestamp) {
        long fullTimeGraphStartTime = getControl().getFullTimeGraphStartTime();
        long fullTimeGraphEndTime = getControl().getFullTimeGraphEndTime();
        return timestampToPaneXPos(timestamp, fullTimeGraphStartTime, fullTimeGraphEndTime, fNanosPerPixel);
    }

    @VisibleForTesting
    public static double timestampToPaneXPos(long timestamp, long start, long end, double nanosPerPixel) {
        if (timestamp < start) {
            throw new IllegalArgumentException(timestamp + " is smaller than trace start time " + start); //$NON-NLS-1$
        }
        if (timestamp > end) {
            throw new IllegalArgumentException(timestamp + " is greater than trace end time " + end); //$NON-NLS-1$
        }

        double traceTimeRange = end - start;
        double timeStampRatio = (timestamp - start) / traceTimeRange;

        long fullTraceWidthInPixels = (long) (traceTimeRange / nanosPerPixel);
        double xPos = fullTraceWidthInPixels * timeStampRatio;
        return Math.round(xPos);
    }

    long paneXPosToTimestamp(double x) {
        long fullTimeGraphStartTime = getControl().getFullTimeGraphStartTime();
        return paneXPosToTimestamp(x, fTimeGraphPane.getWidth(), fullTimeGraphStartTime, fNanosPerPixel);
    }

    @VisibleForTesting
    public static long paneXPosToTimestamp(double x, double totalWidth, long startTimestamp, double nanosPerPixel) {
        if (x < 0.0 || totalWidth < 1.0 || x > totalWidth) {
            throw new IllegalArgumentException("Invalid position arguments: pos=" + x + ", width=" + totalWidth);
        }

        long ts = Math.round(x * nanosPerPixel);
        return ts + startTimestamp;
    }

    private static int paneYPosToEntryListIndex(double yPos, double yMax, int nbEntries) {
        double ratio = yPos / yMax;
        return (int) (ratio * nbEntries);
    }
}