[WIP] CFV Refactor
[deliverable/tracecompass.git] / tmf / org.eclipse.tracecompass.tmf.ui / src / org / eclipse / tracecompass / internal / provisional / tmf / ui / views / timegraph2 / swtjfx / SwtJfxTimeGraphViewer.java
diff --git a/tmf/org.eclipse.tracecompass.tmf.ui/src/org/eclipse/tracecompass/internal/provisional/tmf/ui/views/timegraph2/swtjfx/SwtJfxTimeGraphViewer.java b/tmf/org.eclipse.tracecompass.tmf.ui/src/org/eclipse/tracecompass/internal/provisional/tmf/ui/views/timegraph2/swtjfx/SwtJfxTimeGraphViewer.java
new file mode 100644 (file)
index 0000000..05b824a
--- /dev/null
@@ -0,0 +1,734 @@
+/*******************************************************************************
+ * 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.eclipse.tracecompass.internal.provisional.tmf.ui.views.timegraph2.swtjfx;
+
+import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+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.swt.widgets.Display;
+import org.eclipse.tracecompass.internal.provisional.tmf.core.views.timegraph2.ITimeGraphModelRenderProvider;
+import org.eclipse.tracecompass.internal.provisional.tmf.core.views.timegraph2.TimeGraphModelRender;
+import org.eclipse.tracecompass.internal.provisional.tmf.core.views.timegraph2.TimeGraphStateInterval;
+import org.eclipse.tracecompass.internal.provisional.tmf.core.views.timegraph2.TimeGraphTreeRender;
+import org.eclipse.tracecompass.internal.provisional.tmf.ui.views.timegraph2.TimeGraphModelViewer;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+
+import javafx.beans.value.ChangeListener;
+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 TimeGraphModelViewer {
+
+    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 = checkNotNull(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 = checkNotNull(Color.BLUE);
+    private static final Color SELECTION_FILL_COLOR = checkNotNull(Color.LIGHTBLUE.deriveColor(0, 1.2, 1, 0.4));
+
+    private static final int LABEL_SIDE_MARGIN = 10;
+
+    // ------------------------------------------------------------------------
+    // Class fields
+    // ------------------------------------------------------------------------
+
+    private final SelectionContext fSelectionCtx = new SelectionContext();
+    private final ScrollingContext fScrollingCtx = new ScrollingContext();
+
+    private final LatestJobExecutor fJobExecutor = new LatestJobExecutor();
+
+    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;
+
+    /**
+     * Height of individual entries (text + states), including padding.
+     *
+     * TODO Make this configurable (vertical zoom feature)
+     */
+    private static final double ENTRY_HEIGHT = 20;
+
+
+    /** Current zoom level */
+    private double fNanosPerPixel = 1.0;
+
+
+    /**
+     * Constructor
+     *
+     * @param parent
+     *            Parent SWT composite
+     */
+    public SwtJfxTimeGraphViewer(Composite parent, ITimeGraphModelRenderProvider provider) {
+        super(provider);
+
+        // 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);
+
+        /* 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();
+        getSignalingContext().initializeForTrace(trace);
+    }
+
+    // ------------------------------------------------------------------------
+    // Test accessors
+    // ------------------------------------------------------------------------
+
+    @VisibleForTesting
+    protected Pane getTimeGraphPane() {
+        return fTimeGraphPane;
+    }
+
+    @VisibleForTesting
+    protected ScrollPane getTimeGraphScrollPane() {
+        return fTimeGraphScrollPane;
+    }
+
+    // ------------------------------------------------------------------------
+    // Operations
+    // ------------------------------------------------------------------------
+
+    @Override
+    protected void seekVisibleRangeImpl(long visibleWindowStartTime, long visibleWindowEndTime) {
+        final long fullTimeGraphStart = getFullTimeGraphStartTime();
+        final long fullTimeGraphEnd = 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);
+    }
+
+    @Override
+    protected void paintAreaImpl(ITmfTrace trace, long windowStartTime, long windowEndTime) {
+        final long fullTimeGraphStart = getFullTimeGraphStartTime();
+        final long fullTimeGraphEnd = 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;
+
+        Job job = new Job("Time Graph Update") {
+            @Override
+            protected IStatus run(@Nullable IProgressMonitor monitor) {
+                IProgressMonitor mon = checkNotNull(monitor);
+
+                /* Apply the configuration options to the render provider */
+                ITimeGraphModelRenderProvider renderProvider = getModelRenderProvider();
+                renderProvider.setConfiguredTimeRange(windowStartTime, windowEndTime);
+
+                /*
+                 * 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);
+
+                List<TimeGraphModelRender> renders = new ArrayList<>();
+                long renderStart = renderingStartTime - renderTimeRange;
+                // TODO Find a way to streamize/parallelize this loop?
+                do {
+                    renderStart += renderTimeRange;
+                    long renderEnd = Math.min(renderStart + renderTimeRange, renderingEndTime);
+                    // FIXME Even with /10 we sometimes get holes in the view. Subpixel rendering?
+                    // Needs to be debugged/tested further.
+                    long resolution = Math.max(1, Math.round(fNanosPerPixel / 10));
+
+                    System.out.printf("requesting render from %,d to %,d, resolution=%d%n",
+                            renderStart, renderEnd, resolution);
+
+                    TimeGraphModelRender render = renderProvider.getRender(trace,
+                            renderStart, renderEnd, resolution, monitor);
+                    renders.add(render);
+                } while ((renderStart + renderTimeRange) < renderingEndTime);
+
+                if (mon.isCanceled()) {
+                    /* Job was cancelled, no need to update the UI */
+                    System.out.println("job was cancelled before it could end");
+                    return Status.CANCEL_STATUS;
+                }
+
+                if (renders.isEmpty()) {
+                    /* Nothing to show yet, keep the view empty */
+                    return Status.OK_STATUS;
+                }
+
+                /* Prepare the time graph part */
+                Node timeGraphContents = prepareTimeGraphContents(renders);
+
+                /* Prepare the tree part */
+                Node treeContents = prepareTreeContents(renders.get(0).getTreeRender(), treePaneWidth);
+
+                if (mon.isCanceled()) {
+                    /* Job was cancelled, no need to update the UI */
+                    System.out.println("job was cancelled before it could end");
+                    return Status.CANCEL_STATUS;
+                }
+
+                /* Update the view! */
+                Display.getDefault().syncExec( () -> {
+                    fTreePane.getChildren().clear();
+                    fTreePane.getChildren().add(treeContents);
+
+                    fTimeGraphStatesLayer.getChildren().clear();
+                    fTimeGraphStatesLayer.getChildren().add(timeGraphContents);
+                });
+
+                return Status.OK_STATUS;
+            }
+        };
+
+        fJobExecutor.schedule(job);
+    }
+
+    @Override
+    protected void drawSelectionImpl(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<TimeGraphModelRender> renders) {
+        Set<Node> canvases = renders.stream()
+                .parallel() // order doesn't matter here
+                .flatMap(render -> getCanvasesForRender(render).stream())
+                .collect(Collectors.toSet());
+
+        return new Group(canvases);
+    }
+
+    /**
+     * 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 Collection<Canvas> getCanvasesForRender(TimeGraphModelRender render) {
+        List<List<TimeGraphStateInterval>> stateIntervals = render.getStateIntervals();
+        /* The canvas will be put on the Pane at this offset */
+        final double xOffset = timestampToPaneXPos(render.getStartTime());
+        final double xEnd = timestampToPaneXPos(render.getEndTime());
+        final double canvasWidth = xEnd - xOffset;
+        final int maxEntriesPerCanvas = (int) (MAX_CANVAS_HEIGHT / ENTRY_HEIGHT);
+
+        /*
+         * Split the full list of intervals into smaller partitions, and draw
+         * one Canvas per partition.
+         */
+        List<Canvas> canvases = new ArrayList<>();
+        double yOffset = 0;
+        List<List<List<TimeGraphStateInterval>>> partitionedIntervals =
+                Lists.partition(stateIntervals, maxEntriesPerCanvas);
+        for (int i = 0; i < partitionedIntervals.size(); i++) {
+            /* "states" represent the subset of intervals to draw on this Canvas */
+            List<List<TimeGraphStateInterval>> states = partitionedIntervals.get(i);
+            final double canvasHeight = ENTRY_HEIGHT * states.size();
+
+            Canvas canvas = new Canvas(canvasWidth, canvasHeight);
+            drawBackgroundLines(canvas, ENTRY_HEIGHT);
+            drawStates(states, canvas.getGraphicsContext2D(), xOffset);
+
+//            System.out.println("relocating canvas of size + (" + canvasWidth + ", " + canvasHeight + ") to " + xOffset + ", " + yOffset);
+            canvas.relocate(xOffset, yOffset);
+            canvas.setCache(true); // TODO Test?
+            canvases.add(canvas);
+
+            yOffset += canvasHeight;
+        }
+        return canvases;
+    }
+
+    private void drawStates(List<List<TimeGraphStateInterval>> stateIntervalsToDraw, GraphicsContext gc, double xOffset) {
+        IntStream.range(0, stateIntervalsToDraw.size()).forEach(index -> {
+            /*
+             * The base (top) of each full-thickness rectangle object we will
+             * draw for this entry
+             */
+            final double xBase = index * ENTRY_HEIGHT;
+
+            List<TimeGraphStateInterval> intervals = stateIntervalsToDraw.get(index);
+            for (TimeGraphStateInterval interval : intervals) {
+                try {
+                    /*
+                     * These coordinates are relative to the canvas itself, so
+                     * we need to substract the value of the offset of the
+                     * canvas relative to the Pane.
+                     */
+                    final double xStart = timestampToPaneXPos(interval.getStartEvent().getTimestamp()) - xOffset;
+                    final double xEnd = timestampToPaneXPos(interval.getEndEvent().getTimestamp()) - xOffset;
+                    final double xWidth = Math.max(1.0, xEnd - xStart);
+
+                    double yStart, yHeight;
+                    switch (interval.getLineThickness()) {
+                    case NORMAL:
+                    default:
+                        yStart = xBase + 4;
+                        yHeight = ENTRY_HEIGHT - 4;
+                        break;
+                    case SMALL:
+                        yStart = xBase + 8;
+                        yHeight = ENTRY_HEIGHT - 8;
+                        break;
+                    }
+
+                    gc.setFill(JfxColorFactory.getColorFromDef(interval.getColorDefinition()));
+                    gc.fillRect(xStart, yStart, xWidth, yHeight);
+
+                } catch (IllegalArgumentException iae) { // TODO Temp
+                    System.out.println("out of bounds interval:" + interval.toString());
+                    continue;
+                }
+
+                // TODO Paint the state's name if applicable
+            }
+        });
+
+    }
+
+    // ------------------------------------------------------------------------
+    // 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);
+
+            getSignalingContext().sendTimeRangeSelectionUpdate(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<Object> fHScrollChangeListener = (observable, oldValue, newValue) -> {
+            if (!fUserActionOngoing) {
+                System.out.println("Listener triggered but inactive");
+                return;
+            }
+
+            System.out.println("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();
+            double hvalue = fTimeGraphScrollPane.getHvalue();
+            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);
+
+            getSignalingContext().sendVisibleWindowRangeUpdate(tsStart, tsEnd);
+        };
+    }
+
+    // ------------------------------------------------------------------------
+    // 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();
+    }
+
+    private double timestampToPaneXPos(long timestamp) {
+        return timestampToPaneXPos(timestamp, getFullTimeGraphStartTime(), getFullTimeGraphEndTime(), 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);
+    }
+
+    private long paneXPosToTimestamp(double x) {
+        return paneXPosToTimestamp(x, fTimeGraphPane.getWidth(), getFullTimeGraphStartTime(), 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;
+    }
+
+}
This page took 0.030668 seconds and 5 git commands to generate.