]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.trend/src/org/simantics/trend/impl/TrendParticipant.java
Fixed all line endings of the repository
[simantics/platform.git] / bundles / org.simantics.trend / src / org / simantics / trend / impl / TrendParticipant.java
index 66a67eb62658ba4f4699a309f2ae1c95c303ab35..54109db8732cccbe4bbda1942fec01b269c9ddad 100644 (file)
-/*******************************************************************************\r
- * Copyright (c) 2007, 2011 Association for Decentralized Information Management in\r
- * Industry THTH ry.\r
- * All rights reserved. This program and the accompanying materials\r
- * are made available under the terms of the Eclipse Public License v1.0\r
- * which accompanies this distribution, and is available at\r
- * http://www.eclipse.org/legal/epl-v10.html\r
- *\r
- * Contributors:\r
- *     VTT Technical Research Centre of Finland - initial API and implementation\r
- *******************************************************************************/\r
-package org.simantics.trend.impl;\r
-\r
-import java.awt.Cursor;\r
-import java.awt.Toolkit;\r
-import java.awt.datatransfer.Clipboard;\r
-import java.awt.datatransfer.StringSelection;\r
-import java.awt.geom.Point2D;\r
-import java.awt.geom.Rectangle2D;\r
-import java.awt.print.PrinterException;\r
-import java.io.File;\r
-import java.io.IOException;\r
-\r
-import org.simantics.databoard.accessor.error.AccessorException;\r
-import org.simantics.databoard.util.Bean;\r
-import org.simantics.g2d.canvas.Hints;\r
-import org.simantics.g2d.canvas.ICanvasContext;\r
-import org.simantics.g2d.canvas.IMouseCursorHandle;\r
-import org.simantics.g2d.canvas.impl.AbstractCanvasParticipant;\r
-import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;\r
-import org.simantics.g2d.canvas.impl.HintReflection.HintListener;\r
-import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;\r
-import org.simantics.g2d.chassis.ITooltipProvider;\r
-import org.simantics.g2d.participant.TimeParticipant;\r
-import org.simantics.history.HistoryException;\r
-import org.simantics.history.csv.CSVFormatter;\r
-import org.simantics.history.util.ProgressMonitor;\r
-import org.simantics.scenegraph.g2d.IG2DNode;\r
-import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseExitEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseWheelMovedEvent;\r
-import org.simantics.scenegraph.g2d.events.TimeEvent;\r
-import org.simantics.scenegraph.g2d.events.command.CommandEvent;\r
-import org.simantics.scenegraph.g2d.events.command.Commands;\r
-import org.simantics.trend.configuration.ItemPlacement;\r
-import org.simantics.trend.configuration.TrendItem.Renderer;\r
-import org.simantics.trend.util.PrintUtil;\r
-import org.simantics.utils.datastructures.hints.IHintContext.Key;\r
-import org.simantics.utils.datastructures.hints.IHintContext.KeyOf;\r
-import org.simantics.utils.datastructures.hints.IHintObservable;\r
-\r
-public class TrendParticipant extends AbstractCanvasParticipant {\r
-\r
-    /**\r
-     * Key for storing chart value tip box relative position. Changed by\r
-     * {@link TrendParticipant} after user finishes dragging this class around.\r
-     * */\r
-    public static final Key KEY_VALUE_TIP_BOX_RELATIVE_POS = new KeyOf(Point2D.class);\r
-\r
-    /** Key for chart redraw interval in milliseconds (drawn only if dirty) */\r
-    public static final Key KEY_TREND_DRAW_INTERVAL = new KeyOf(Long.class);\r
-\r
-    /** The time code when trend was last redrawn */\r
-    public static final Key KEY_TREND_SHAPE_LAST = new KeyOf(Long.class);\r
-\r
-    /** Key for interval time of value scale */\r
-    public static final Key KEY_TREND_AUTOSCALE_INTERVAL = new KeyOf(Long.class);\r
-\r
-    /** Key for time of last autoscale */\r
-    public static final Key KEY_TREND_AUTOSCALE_LAST = new KeyOf(Long.class);\r
-\r
-    public static final double KEY_MOVE = 0.33;\r
-\r
-    /** Cursor when panning */\r
-    public MouseCursors cursors = new MouseCursors();\r
-\r
-    @HintListener(Class=Hints.class, Field="KEY_CONTROL_BOUNDS")\r
-    public void selectionChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {\r
-       trend.shapedirty = true;\r
-       setDirty();\r
-    }\r
-\r
-    TrendNode trend;\r
-\r
-    @Dependency TimeParticipant time;\r
-\r
-    Grab grab; \r
-\r
-    // The single item on which the mouse hovers over is set to this field. (Binary items are selectable). \r
-    public ItemNode hoveringItem;\r
-\r
-    @Override\r
-    public void addedToContext(ICanvasContext ctx) {\r
-        super.addedToContext(ctx);\r
-        time.registerForEvents(getClass());\r
-    }\r
-\r
-    public void setTrend(TrendNode node) {\r
-        trend = node;\r
-    }\r
-\r
-    public TrendNode getTrend() {\r
-        return trend;\r
-    }\r
-\r
-//    @SGInit\r
-//    public void initSG(G2DParentNode parent) {\r
-//     Set<TrendNode> nodes = NodeUtil.collectNodes(parent, TrendNode.class);\r
-//     trends.addAll(nodes);\r
-//    }\r
-\r
-    @SGCleanup\r
-    public void cleanupSG() {\r
-    }\r
-\r
-    protected void updateNode() {\r
-//     titleNode.setText(text)\r
-//        node.setEnabled(isPaintingEnabled());\r
-//        node.setGridColor(getGridColor());\r
-//        node.setGridSize(getGridSize());\r
-    }\r
-\r
-    @EventHandler(priority = 0)\r
-    public boolean handleTimeEvent(TimeEvent e) {\r
-        //System.out.println("(" + isRemoved() + ") time event: " + e.time + " (" + e.interval + ")");\r
-        if (isRemoved()) {\r
-            if (time != null)\r
-                time.unregisterForEvents(getClass());\r
-            return false;\r
-        }\r
-\r
-        long currentTime = e.time;\r
-\r
-        Long drawInterval = getHint(KEY_TREND_DRAW_INTERVAL);\r
-        if (drawInterval == null) drawInterval = 200L;\r
-        Long lastDrawTime = getHint(KEY_TREND_SHAPE_LAST);\r
-        boolean drawIntervalElapsed = lastDrawTime == null || (currentTime>=lastDrawTime+drawInterval);\r
-        if (drawIntervalElapsed) {\r
-//            System.out.println("elapsed="+drawIntervalElapsed+" currentTime="+currentTime+" lastDraw="+lastDrawTime+", interval="+drawInterval+", nextDrawTime="+(lastDrawTime+drawInterval));\r
-//            System.out.println("elapsed="+drawIntervalElapsed+" currentTime="+currentTime+" lastDraw="+lastDrawTime+", interval="+drawInterval);\r
-            setHint(KEY_TREND_SHAPE_LAST, currentTime);\r
-            trend.shapedirty |= trend.datadirty; \r
-            trend.datadirty = false;\r
-        }\r
-\r
-        // Scale time\r
-        Long autoscaleInterval = getHint(KEY_TREND_AUTOSCALE_INTERVAL);\r
-        if (autoscaleInterval == null) autoscaleInterval = 1000L;\r
-        Long autoscaleTime = getHint(KEY_TREND_AUTOSCALE_LAST);\r
-        boolean autoscale = autoscaleTime==null || (currentTime>=autoscaleTime+autoscaleInterval);\r
-        if (autoscale) {\r
-            boolean l = trend.autoscale(trend.autoscaletime, autoscale);\r
-            if (!draggingValueTip(grab)) {\r
-                trend.updateValueTipTime();\r
-            }\r
-            if (l) {\r
-                trend.layout();\r
-            }\r
-\r
-            setHint(KEY_TREND_AUTOSCALE_LAST, currentTime);\r
-        }\r
-\r
-        // IT IS DRAW TIME\r
-        if (trend.shapedirty) {\r
-//            System.out.println(currentTime+": Draw time!");\r
-            setDirty();\r
-        }\r
-\r
-        return false;\r
-    }\r
-\r
-    @EventHandler(priority = 0)\r
-    public boolean mouseClick(MouseClickEvent me) {\r
-               IG2DNode node = trend.pickNode( me.controlPosition );\r
-               \r
-       if (me.clickCount==2 && node!=null) {\r
-               trend.valueTipTime = trend.prevValueTipTime;\r
-                       if (node instanceof Plot) {\r
-                               boolean binaryArea = me.controlPosition.getY()>trend.plot.getY()+trend.plot.analogAreaHeight;\r
-                               if (binaryArea) {\r
-                                       trend.horizRuler.zoomOut();\r
-                               } else {\r
-                                       trend.zoomOut();\r
-                               }\r
-                               setDirty();\r
-                               return true;\r
-                       } else if (node instanceof HorizRuler) {\r
-                               HorizRuler hr = (HorizRuler) node;\r
-                               hr.zoomOut();\r
-                               setDirty();\r
-                               return true;\r
-                       } else if (node instanceof VertRuler) {\r
-                               VertRuler vr = (VertRuler) node;\r
-                               vr.zoomOut();\r
-                               setDirty();\r
-                               return true;\r
-                       }\r
-       }\r
-       \r
-       // Click ValueTip \r
-       if (me.clickCount==1 && me.button==MouseEvent.LEFT_BUTTON && node!=null && node instanceof Plot) {\r
-               trend.prevValueTipTime = trend.valueTipTime;\r
-                       if (trend.valueTipTime == null) {\r
-                       trend.valueTipTime = Double.isNaN(trend.mouseHoverTime)?null:trend.mouseHoverTime;\r
-                       trend.valueTipHover = true;\r
-                       } else {\r
-                               double valueTipX = trend.horizRuler.toX( trend.valueTipTime );\r
-                       double x = me.controlPosition.getX() - trend.horizRuler.getBounds().getX();\r
-                       boolean hit = x>=valueTipX-TrendLayout.VALUE_TIP_HOVER_SENSITIVITY && x<=valueTipX+TrendLayout.VALUE_TIP_HOVER_SENSITIVITY;\r
-                       if (hit) trend.valueTipTime = null;\r
-                       }\r
-                       setDirty();\r
-       }\r
-\r
-       /*\r
-       // Right click removes value tip\r
-       if (me.clickCount==1 && me.button==MouseEvent.RIGHT_BUTTON) {\r
-                       if (trend.valueTipTime != null) {\r
-                               trend.valueTipTime = null;\r
-                               setDirty();\r
-                       }               \r
-       }*/\r
-\r
-       if (me.clickCount==1 && me.button==MouseEvent.LEFT_BUTTON) {\r
-                       if (node instanceof VertRuler) {\r
-                               VertRuler vr = (VertRuler) node;\r
-                               trend.selectVertRuler( trend.vertRulers.indexOf(vr) );\r
-                               setDirty();\r
-                       } else if (node == null || node instanceof TextNode) {\r
-                               Plot p = trend.plot;\r
-                               double x = me.controlPosition.getX() - p.getX();\r
-                               double y = me.controlPosition.getY() - p.getY();\r
-                               if ( x>=0 && x<=p.getWidth() && y<=0 && y>=-Plot.DIAMOND_SIZE*2) {\r
-                                       // Click hits milestone area\r
-                                       milestoneSearch: for ( Milestone ms : trend.milestones.milestones ) {\r
-                                               double mx = trend.horizRuler.toX( ms.time );\r
-                                               if ( x>=mx-Plot.DIAMOND_SIZE && x<=mx+Plot.DIAMOND_SIZE ) {\r
-                                                       ITooltipProvider tp = getContext().getTooltipProvider();\r
-                                                       if (tp!=null) {\r
-                                                               tp.show(ms.label, ms.description);\r
-                                                       }\r
-                                                       break milestoneSearch;\r
-                                               }\r
-                                       }\r
-                               }\r
-\r
-                       }\r
-\r
-       }\r
-               return false;\r
-    }\r
-\r
-    @EventHandler(priority = 0)\r
-    public boolean mouseDown(MouseButtonPressedEvent me) {\r
-       // Start Box-Zoom\r
-       if (me.button==MouseEvent.LEFT_BUTTON) {\r
-                       IG2DNode node = trend.pickNode( me.controlPosition );   \r
-                       if (node == null || node instanceof Plot) {\r
-\r
-                // Start value box grab\r
-                {\r
-                    double x = me.controlPosition.getX() - trend.horizRuler.getBounds().getX();\r
-                    double y = me.controlPosition.getY() - trend.plot.getBounds().getY();\r
-                    if (trend.plot.valueTipBoxBounds.contains(x, y)) {\r
-                        //System.out.println("grabbed value box @ " + x + ", " + y);\r
-                        grab = new Grab();\r
-                        grab.valueBoxRelPos.setLocation(trend.spec.viewProfile.valueViewPositionX, trend.spec.viewProfile.valueViewPositionY);\r
-                        grab.valueBoxPos.setFrame(trend.plot.valueTipBoxBounds);\r
-                        grab.mousepos = new Point2D.Double( me.controlPosition.getX(), me.controlPosition.getY() );\r
-                        grab.valueBoxGrab = true;\r
-                        return true;\r
-                    }\r
-                }\r
-\r
-                               // Start Value-Tip grab\r
-                               if (trend.valueTipTime!=null) {\r
-                                       double valueTipX = trend.horizRuler.toX( trend.valueTipTime );\r
-                               double x = me.controlPosition.getX() - trend.horizRuler.getBounds().getX();\r
-                               boolean hit = x>=valueTipX-TrendLayout.VALUE_TIP_HOVER_SENSITIVITY && x<=valueTipX+TrendLayout.VALUE_TIP_HOVER_SENSITIVITY; \r
-                               if (hit) {\r
-                                       grab = new Grab();\r
-                                       grab.mousepos = new Point2D.Double( me.controlPosition.getX(), me.controlPosition.getY() );\r
-                                       grab.sx = trend.horizRuler.unitsPerPixel();\r
-                                       grab.sy = new double[ trend.vertRulers.size() ];\r
-                                       grab.valueTipGrab = true;\r
-                                       return true;\r
-                               }\r
-                               }\r
-\r
-                       // Start Box-Zoom\r
-                               boolean binaryArea = me.controlPosition.getY()>trend.plot.getY()+trend.plot.analogAreaHeight;\r
-                               boolean timeZoom = (me.stateMask & MouseEvent.SHIFT_MASK)>0 || binaryArea;\r
-                               if (trend.selection==null) {\r
-                                       trend.selection = trend.addNode("Selection", SelectionNode.class);\r
-                                       trend.selection.setZIndex( 10 );\r
-                                       trend.selection.start( me.controlPosition, binaryArea, timeZoom );\r
-                               }\r
-                       return true;\r
-                       }\r
-       }\r
-\r
-       // Start grab\r
-               IG2DNode node = trend.pickNode( me.controlPosition );\r
-       if ( (me.button==MouseEvent.MIDDLE_BUTTON) && \r
-                       node!=null && node instanceof Plot) {\r
-                       //Plot p = (Plot) node;\r
-                       TrendNode trend = (TrendNode) node.getParent();\r
-                       boolean shift = (me.stateMask & MouseEvent.SHIFT_MASK)>0;\r
-                       boolean alt = (me.stateMask & MouseEvent.ALT_MASK)>0;\r
-                       //boolean analogArea = me.controlPosition.getY() < p.getY()+p.analogAreaHeight;\r
-                       //boolean binaryArea = me.controlPosition.getY() >= p.getY()+p.analogAreaHeight;\r
-               grab = new Grab();\r
-               grab.mousepos = new Point2D.Double( me.controlPosition.getX(), me.controlPosition.getY() );\r
-               grab.sx = trend.horizRuler.unitsPerPixel();\r
-               grab.sy = new double[ trend.vertRulers.size() ];\r
-               grab.horiz = !shift || alt;\r
-               grab.vert = shift || alt;\r
-               if (grab.vert) {\r
-                       for (int i=0; i<trend.vertRulers.size(); i++)\r
-                               grab.sy[i] = trend.vertRulers.get(i).unitsPerPixel();\r
-               }\r
-               Cursor c = grab.horiz ? (grab.vert ? cursors.grab : cursors.grab_horiz) : (grab.vert ? cursors.grab_vert : cursors.grab); \r
-               grab.cursor = getContext().getMouseCursorContext().setCursor(me.mouseId, c);\r
-               grab.mouseButton = me.button;\r
-               grab.plot = true;\r
-                       trend.horizRuler.translate(0);\r
-                       setHoverTime(null);\r
-               setDirty();\r
-                       return true;\r
-       }\r
-               if ( (me.button==MouseEvent.LEFT_BUTTON||me.button==MouseEvent.MIDDLE_BUTTON) && \r
-                               node!=null && node instanceof HorizRuler) {\r
-                       HorizRuler hr = (HorizRuler) node;\r
-               grab = new Grab();\r
-               grab.cursor = getContext().getMouseCursorContext().setCursor(me.mouseId, cursors.grab_horiz);\r
-               grab.mousepos = new Point2D.Double();\r
-               grab.mousepos.setLocation( me.controlPosition );\r
-               grab.sx = hr.unitsPerPixel();\r
-               grab.horizRuler = true;\r
-               grab.mouseButton = me.button;\r
-               grab.horiz = true;\r
-                       trend.horizRuler.translate(0);\r
-               setDirty();\r
-                       return true;\r
-               }\r
-               if ( (me.button==MouseEvent.LEFT_BUTTON||me.button==MouseEvent.MIDDLE_BUTTON) && \r
-                               node!=null && node instanceof VertRuler) {\r
-                       //VertRuler vr = (VertRuler) node;\r
-               grab = new Grab();\r
-               grab.cursor = getContext().getMouseCursorContext().setCursor(me.mouseId, cursors.grab_vert);\r
-               grab.mousepos = new Point2D.Double();\r
-               grab.mousepos.setLocation( me.controlPosition );\r
-               grab.sy = new double[ trend.vertRulers.size() ];\r
-               for (int i=0; i<trend.vertRulers.size(); i++)\r
-                       grab.sy[i] = trend.vertRulers.get(i).unitsPerPixel();\r
-               grab.mouseButton = me.button;\r
-               grab.vertRuler = trend.vertRulers.indexOf(node);\r
-                       trend.selectVertRuler( grab.vertRuler );\r
-               setDirty();\r
-                       return true;\r
-       }\r
-       \r
-       return false;\r
-    }\r
-\r
-    @EventHandler(priority = 0)\r
-    public boolean mouseUp(MouseButtonReleasedEvent me) {\r
-\r
-        // Release value box grab\r
-        if (me.button==1 && draggingValueBox(grab)) {\r
-            setHint(KEY_VALUE_TIP_BOX_RELATIVE_POS, new Point2D.Double(\r
-                    trend.spec.viewProfile.valueViewPositionX,\r
-                    trend.spec.viewProfile.valueViewPositionY));\r
-            grab = null;\r
-        }\r
-\r
-        // Release value tip grab\r
-        if (me.button==1 && draggingValueTip(grab)) {\r
-            grab = null;\r
-        }\r
-\r
-       // Release Box-Zoom\r
-       if (me.button==1 && trend.selection!=null) {\r
-               Rectangle2D rect = trend.selection.rect;\r
-               \r
-               if (rect.getWidth()>1 && rect.getHeight()>1) {    \r
-                       if (trend.selection.timeZoom) {\r
-                               trend.horizRuler.zoomIn( rect.getX()-trend.horizRuler.getX(), rect.getWidth() );\r
-                       } else {\r
-                               trend.zoomIn( \r
-                                               rect.getX()-trend.plot.getX(), \r
-                                               rect.getY()-trend.plot.getY(),\r
-                                               rect.getWidth(), rect.getHeight(), true, true );\r
-                       }\r
-                       trend.layout();\r
-                               trend.shapedirty = true;\r
-                       setDirty();\r
-               }\r
-               trend.selection.delete();\r
-               trend.selection = null;\r
-\r
-               return true;\r
-       }\r
-       \r
-       if (grab!=null && grab.mouseButton==me.button) {\r
-               grab.cursor.remove();\r
-               grab = null;\r
-       }\r
-       return false;\r
-    }\r
-\r
-    @EventHandler(priority = 0)\r
-    public boolean mouseExit(MouseExitEvent me) {\r
-               trend.valueTipHover = false;\r
-       setHoverTime( null );\r
-       return false;\r
-    }\r
-\r
-    void setHoverTime(Double time) {\r
-       if ( time==null && Double.isNaN(trend.mouseHoverTime) ) return;\r
-       if ( time!=null && trend.mouseHoverTime==time ) return;\r
-       if ( time==null ) {\r
-                       trend.mouseHoverTime = Double.NaN;\r
-       } else {\r
-               trend.mouseHoverTime = time;\r
-               trend.lastMouseHoverTime = time;\r
-       }\r
-       setDirty();\r
-    }\r
-\r
-//    @EventHandler(priority = 0)\r
-//    public boolean mouseEnter(MouseExitEvent me) {\r
-//     //double x = me.controlPosition.getX() - trend.horizRuler.getBounds().getX();\r
-//     //double time = trend.horizRuler.toTime( x ) - trend.horizRuler.basetime;\r
-//     return false;\r
-//    }\r
-\r
-    @SuppressWarnings("unused")\r
-    @EventHandler(priority = 0)\r
-    public boolean mouseMoved(MouseMovedEvent me) {\r
-//        ITooltipProvider tt = getContext().getTooltipProvider();\r
-//        if (tt!=null) {\r
-//            tt.setTooltipText("Hello");\r
-//        }\r
-\r
-               IG2DNode pick = trend.pickNode( me.controlPosition );\r
-       \r
-               TrackMouseMovement: {\r
-                       if ( pick == trend.plot ) {\r
-                       double x = me.controlPosition.getX() - trend.horizRuler.getBounds().getX();\r
-                       double time = trend.horizRuler.toTime( x );\r
-                       double sx = (trend.horizRuler.end-trend.horizRuler.from) / trend.horizRuler.getWidth();\r
-                       double timeSnapTolerance = sx * 7.0; \r
-                               boolean shift = (me.stateMask & MouseEvent.SHIFT_MASK)>0;\r
-                       \r
-                               // SNAP - When shift is held\r
-                       if (shift) {\r
-                               Double snappedTime = null;\r
-                                       try {\r
-                                               snappedTime = trend.snapToValue( time, timeSnapTolerance );\r
-                                               if (snappedTime != null) time = snappedTime;\r
-                                       } catch (HistoryException e) {\r
-                                       } catch (AccessorException e) {\r
-                                       }\r
-                       }\r
-                               \r
-                               setHoverTime(time);\r
-                       }\r
-               }\r
-\r
-        // Implement value box moving while dragging.\r
-        if (draggingValueBox(grab)) {\r
-            Rectangle2D plotBox = trend.plot.getBounds();\r
-            Rectangle2D valueBoxPos = grab.valueBoxPos;\r
-            Rectangle2D valueBox = trend.plot.valueTipBoxBounds;\r
-            double dx = me.controlPosition.getX() - grab.mousepos.getX();\r
-            double dy = me.controlPosition.getY() - grab.mousepos.getY();\r
-            double margin = Plot.VALUE_TIP_BOX_PLOT_MARGIN;\r
-            double maxX = plotBox.getWidth() - margin - valueBox.getWidth();\r
-            double maxY = plotBox.getHeight() - margin - valueBox.getHeight();\r
-            double boxX = valueBoxPos.getX() + dx;\r
-            double boxY = valueBoxPos.getY() + dy;\r
-            double pw = plotBox.getWidth() - 2 * margin;\r
-            double ph = plotBox.getHeight() - 2 * margin;\r
-            double w = pw - valueBox.getWidth();\r
-            double h = ph - valueBox.getHeight();\r
-            if (w > 0 && h > 0) {\r
-                double rx = (boxX - margin) / w;\r
-                double ry = (boxY - margin) / h;\r
-                rx = Math.max(0, Math.min(rx, 1));\r
-                ry = Math.max(0, Math.min(ry, 1));\r
-                trend.spec.viewProfile.valueViewPositionX = rx;\r
-                trend.spec.viewProfile.valueViewPositionY = ry;\r
-                setDirty();\r
-            }\r
-            return false;\r
-        }\r
-\r
-        ValueToolTip: if ( pick == trend.plot) {\r
-            if (draggingValueTip(grab)) {\r
-                               // Move grabbed value-tip\r
-                               trend.valueTipTime = Double.isNaN(trend.mouseHoverTime) ? null : trend.mouseHoverTime;\r
-                               return false;\r
-                       } else {\r
-                               // Track hover color\r
-                               if (trend.valueTipTime!=null) {\r
-                                       double valueTipX = trend.horizRuler.toX( trend.valueTipTime );\r
-                               double x = me.controlPosition.getX() - trend.horizRuler.getBounds().getX();\r
-                               boolean hit = x>=valueTipX-TrendLayout.VALUE_TIP_HOVER_SENSITIVITY && x<=valueTipX+TrendLayout.VALUE_TIP_HOVER_SENSITIVITY;\r
-                               trend.valueTipHover = hit;\r
-                               }\r
-                       }\r
-       }\r
-               \r
-               // Item pick\r
-               ItemPick: {\r
-               if ( pick instanceof VertRuler ) {\r
-                       VertRuler vr = (VertRuler) pick;\r
-                       hoveringItem = null;\r
-                       for (ItemNode item : trend.analogItems) {\r
-                               if (item.item.renderer != Renderer.Analog || item.ruler==vr) {\r
-                                       if ( hoveringItem != null) break;\r
-                                       hoveringItem = item;\r
-                               }\r
-                       }\r
-               } else if ( pick == null || pick instanceof Plot ) {\r
-                       hoveringItem = trend.plot.pickItem( me.controlPosition );\r
-               } else {\r
-                       hoveringItem = null;\r
-               }\r
-       }\r
-       \r
-       // Box-Zoom\r
-       if (trend.selection!=null) {\r
-               trend.selection.setEndPoint( me.controlPosition );\r
-               setHoverTime(null);\r
-               setDirty();\r
-               return true;\r
-       }\r
-       \r
-       // Drag axis\r
-       if (grab != null) {\r
-               double dx = me.controlPosition.getX() - grab.mousepos.x;\r
-               double dy = me.controlPosition.getY() - grab.mousepos.y;\r
-               grab.mousepos.setLocation(me.controlPosition);\r
-               \r
-               if (grab.plot) {\r
-                       if (grab.horiz) {\r
-                               trend.horizRuler.translate(-dx*grab.sx);\r
-                       }\r
-                       if (grab.vert) {\r
-                               for (int i=0; i<grab.sy.length; i++) {\r
-                                       if (i>=trend.vertRulers.size()) break;\r
-                                       trend.vertRulers.get(i).translate(dy*grab.sy[i]);\r
-                               }\r
-                       }\r
-               } else if (grab.horizRuler) {\r
-                       trend.horizRuler.translate(-dx*grab.sx);\r
-               } else if (grab.vertRuler>=0) {\r
-                       if (grab.vertRuler<=trend.vertRulers.size()) {\r
-                               VertRuler vr = trend.vertRulers.get( grab.vertRuler );\r
-                               trend.selectVertRuler( grab.vertRuler );\r
-                               vr.translate(dy*grab.sy[ grab.vertRuler ]);\r
-                       }\r
-               }\r
-                       trend.layout();\r
-               trend.shapedirty = true;\r
-               setDirty();\r
-               return true;\r
-       }\r
-       return false;\r
-    }\r
-\r
-    @EventHandler(priority = 0)\r
-    public boolean mouseWheel(MouseWheelMovedEvent me) {\r
-               IG2DNode node = trend.pickNode( me.controlPosition );\r
-               if (node instanceof Plot || node instanceof HorizRuler || node instanceof VertRuler) {\r
-                       Point2D pt = node.parentToLocal( me.controlPosition );\r
-                       boolean shift = (me.stateMask & MouseEvent.SHIFT_MASK)>0;\r
-                       boolean alt = (me.stateMask & MouseEvent.ALT_MASK)>0;\r
-                       double pw = trend.plot.getWidth();\r
-                       double ph = trend.plot.analogAreaHeight;\r
-\r
-                       double r = Math.pow(0.9, me.wheelRotation);\r
-\r
-                       double w = r * pw; \r
-                       double h = r * ph;\r
-                       double rx = pt.getX() / pw;\r
-                       double ry = pt.getY() / ph;\r
-\r
-                       double zx = (pw-w)*rx;\r
-                       double zy = (ph-h)*ry;\r
-\r
-                       if (node instanceof Plot) {\r
-                               TrendNode trend = (TrendNode) node.getParent();\r
-                               if (shift||alt) {\r
-                                       for (VertRuler vr : trend.vertRulers) {\r
-                                               vr.zoomIn(zy, h);\r
-                                       }\r
-                               } \r
-                               if (!shift) {\r
-                                       trend.horizRuler.zoomIn(zx, w);\r
-                               }\r
-                       }\r
-\r
-                       if (node instanceof HorizRuler) {\r
-                               //HorizRuler hr = (HorizRuler) node;\r
-                               trend.horizRuler.zoomIn(zx, w);\r
-                       }\r
-\r
-                       if (node instanceof VertRuler) {\r
-                               VertRuler vr = (VertRuler) node;\r
-                               trend.selectVertRuler( trend.vertRulers.indexOf(vr) );\r
-                               vr.zoomIn(zy, h);\r
-                       }\r
-                       trend.shapedirty = true;\r
-                       trend.layout();\r
-                       setDirty();\r
-                       return true;\r
-               }\r
-       return false;\r
-    }\r
-\r
-    @EventHandler(priority = 0)\r
-    public boolean handleCommandEvent(CommandEvent e) {\r
-       \r
-       if (e.command == Commands.CANCEL) {\r
-               if (trend.selection != null) {\r
-                       trend.selection.delete();\r
-                       trend.selection = null;\r
-                       setDirty();\r
-                       return true;\r
-               }\r
-               \r
-               if (grab!=null) {\r
-                if (draggingValueBox(grab)) {\r
-                    trend.spec.viewProfile.valueViewPositionX = grab.valueBoxRelPos.getX();\r
-                    trend.spec.viewProfile.valueViewPositionY = grab.valueBoxRelPos.getY();\r
-                }\r
-                       if (grab.cursor!=null) grab.cursor.remove();\r
-                       grab = null;\r
-                       setDirty();\r
-                       return true;\r
-               }\r
-               \r
-               if (trend.valueTipTime != null) {\r
-                       trend.valueTipTime = null;\r
-                       setDirty();\r
-                       return true;\r
-               }\r
-               return false;\r
-       }\r
-       \r
-       if (e.command == Commands.PAN_LEFT) {\r
-                       double pw = trend.plot.getWidth();\r
-                       double ph = trend.plot.analogAreaHeight;\r
-                       double zx = -pw * KEY_MOVE;\r
-                       double zy = 0;\r
-                       trend.zoomIn(zx, zy, pw, ph, true, true);\r
-                       trend.layout();\r
-                       setDirty();\r
-       }\r
-       \r
-       if (e.command == Commands.PAN_RIGHT) {\r
-                       double pw = trend.plot.getWidth();\r
-                       double ph = trend.plot.analogAreaHeight;\r
-                       double zx = +pw * KEY_MOVE;\r
-                       double zy = 0;\r
-                       trend.zoomIn(zx, zy, pw, ph, true, true);\r
-                       trend.horizRuler.autoscroll = false;\r
-                       trend.layout();\r
-                       setDirty();\r
-       }\r
-       \r
-       if (e.command == Commands.PAN_UP) {\r
-                       double pw = trend.plot.getWidth();\r
-                       double ph = trend.plot.analogAreaHeight;\r
-                       double zx = 0;\r
-                       double zy = -ph * KEY_MOVE;\r
-                       trend.zoomIn(zx, zy, pw, ph, true, true);\r
-                       trend.horizRuler.autoscroll = false;\r
-                       trend.layout();\r
-                       setDirty();\r
-       }\r
-       \r
-       if (e.command == Commands.PAN_DOWN) {\r
-                       double pw = trend.plot.getWidth();\r
-                       double ph = trend.plot.analogAreaHeight;\r
-                       double zx = 0;\r
-                       double zy = +ph * KEY_MOVE;\r
-                       trend.zoomIn(zx, zy, pw, ph, true, true);\r
-                       trend.horizRuler.autoscroll = false;\r
-                       trend.layout();\r
-                       setDirty();\r
-       }\r
-       \r
-       // Zoom out to time window settings (VK_4)\r
-       if (e.command.equals(Commands.AUTOSCALE)) {\r
-                       trend.horizRuler.autoscroll = true;\r
-                       for (VertRuler vertRuler : trend.vertRulers) vertRuler.autoscroll = true;\r
-                       trend.zoomOut();\r
-                       trend.layout();\r
-                       setDirty();\r
-       }\r
-       \r
-       // Fit all visible (VK_1)\r
-       if (e.command.equals(Commands.ZOOM_TO_FIT)) {\r
-                       trend.readMinMaxFromEnd();\r
-                       trend.horizRuler.setFromEnd(trend.horizRuler.iFrom, trend.horizRuler.iEnd);\r
-                       trend.horizRuler.fireListener();\r
-                       int c = trend.vertRulers.size();\r
-                       for (int i=0; i<c; i++) {\r
-                               VertRuler vr = trend.vertRulers.get(c-i-1);\r
-                               double nMin = vr.iMin;\r
-                               double nMax = vr.iMax;\r
-\r
-                               double diff = nMax - nMin;\r
-                               if (diff==0.0) {\r
-                                       nMin -= 0.5;\r
-                                       nMax += 0.5;\r
-                                       diff = nMax - nMin;\r
-                               }\r
-                               double margin = diff*0.02;\r
-                               \r
-                               if (trend.itemPlacement == ItemPlacement.Stacked) {\r
-                                       nMin = nMin - (diff)*i - margin;\r
-                                       nMax = nMax + (diff)*(c-i-1) + margin;\r
-                                       \r
-                               } else {\r
-                                       nMin = vr.iMin - margin;\r
-                                       nMax = vr.iMax + margin;\r
-                               }\r
-                               \r
-                               vr.zoomTo(nMin, nMax);\r
-                       }\r
-                       trend.horizRuler.autoscroll = false;\r
-                       trend.layout();\r
-                       setDirty();\r
-                       return true;\r
-       }\r
-\r
-       // Fit horiz (VK_2)\r
-       if (e.command.equals(Commands.ZOOM_TO_FIT_HORIZ)) {\r
-                       trend.readMinMaxFromEnd();\r
-                       trend.horizRuler.setFromEnd(trend.horizRuler.iFrom, trend.horizRuler.iEnd);\r
-                       trend.layout();\r
-                       trend.horizRuler.autoscroll = false;\r
-                       setDirty();\r
-                       return true;\r
-       }\r
-\r
-       // Fit vert (VK_3)\r
-       if (e.command.equals(Commands.ZOOM_TO_FIT_VERT)) {\r
-                       trend.readMinMaxFromEnd();\r
-                       int c = trend.vertRulers.size();\r
-                       for (int i=0; i<c; i++) {\r
-                               VertRuler vr = trend.vertRulers.get(c-i-1);\r
-                               double nMin = vr.iMin;\r
-                               double nMax = vr.iMax;\r
-\r
-                               double diff = nMax - nMin;\r
-                               if (diff==0.0) {\r
-                                       nMin -= 0.5;\r
-                                       nMax += 0.5;\r
-                                       diff = nMax - nMin;\r
-                               }\r
-                               double margin = diff*0.02;\r
-                               \r
-                               if (trend.itemPlacement == ItemPlacement.Stacked) {\r
-                                       nMin = nMin - (diff)*i - margin;\r
-                                       nMax = nMax + (diff)*(c-i-1) + margin;\r
-                                       \r
-                               } else {\r
-                                       nMin = vr.iMin - margin;\r
-                                       nMax = vr.iMax + margin;\r
-                               }\r
-                               \r
-                               vr.zoomTo(nMin, nMax);\r
-                       }\r
-                       trend.layout();\r
-                       setDirty();\r
-                       return true;\r
-       }\r
-       \r
-       // +\r
-       if (e.command == Commands.ZOOM_IN) {\r
-                       double pw = trend.plot.getWidth();\r
-                       double ph = trend.plot.analogAreaHeight;\r
-                       double za = 3; // Zoom amount\r
-                       double w = (1. - (0.1*za)) * pw; \r
-                       double h = (1. - (0.1*za)) * ph;\r
-\r
-                       double zx = (pw-w)/2;\r
-                       double zy = (ph-h)/2;\r
-                       \r
-                       trend.zoomIn(zx, zy, w, h, true, true);\r
-                       trend.horizRuler.autoscroll = false;\r
-                       trend.layout();\r
-                       setDirty();\r
-                       return true;\r
-       }\r
-       \r
-       // -\r
-       if (e.command == Commands.ZOOM_OUT) {\r
-                       double pw = trend.plot.getWidth();\r
-                       double ph = trend.plot.analogAreaHeight;\r
-                       double za = -3; // Zoom amount\r
-                       double w = (1. - (0.1*za)) * pw; \r
-                       double h = (1. - (0.1*za)) * ph;\r
-\r
-                       double zx = (pw-w)/2;\r
-                       double zy = (ph-h)/2;\r
-                       \r
-                       trend.zoomIn(zx, zy, w, h, true, true);\r
-                       trend.horizRuler.autoscroll = false;\r
-                       trend.layout();\r
-                       setDirty();\r
-                       return true;\r
-       }\r
-       \r
-               // Print to printer\r
-               if (e.command == Commands.PRINT) {\r
-                       try {\r
-                               PrintUtil pu = new PrintUtil();\r
-                               pu.addTrendPage(trend);\r
-                               pu.print();\r
-                       } catch (PrinterException e1) {\r
-                               e1.printStackTrace();\r
-                       }\r
-                       return true;\r
-               }\r
-               \r
-               // Print to PDF\r
-               if (e.command == Commands.PDFPRINT) {\r
-                       PrintUtil pu = new PrintUtil();\r
-                       pu.addTrendPage(trend);\r
-                       try {\r
-                               File f = File.createTempFile("Trend", ".pdf");\r
-                               pu.printPdf(f);\r
-                               System.out.println("Printed Trend to "+f);\r
-                       } catch (IOException e1) {\r
-                               e1.printStackTrace();\r
-                       }\r
-                       return true;\r
-               }\r
-               \r
-               // Read visible values into CSV \r
-               if (e.command == Commands.COPY || e.command == Commands.EXPORT) {\r
-                       StringBuilder sb = new StringBuilder(64*1024);\r
-                       try {\r
-                               // TODO: fix to use preferences.\r
-                               CSVFormatter formatter = new CSVFormatter();\r
-                               formatter.setTimeRange(trend.horizRuler.from, trend.horizRuler.end);\r
-                               for (ItemNode i : trend.allItems)\r
-                               {\r
-                                       if (i.item.hidden) continue;\r
-                                       if (i.historyItems==null || i.historyItems.length==0) continue;\r
-                                       Bean bestQualityStream = i.historyItems[0];\r
-                                       String historyItemId = (String) bestQualityStream.getFieldUnchecked("id");\r
-                                       formatter.addItem( trend.historian, historyItemId, i.item.simpleLabel, i.item.variableReference, i.item.unit);\r
-                               }\r
-                               formatter.sort();\r
-                               try {\r
-                                   // TODO: Use a proper user-cancelable progressmonitor\r
-                                       formatter.formulate2(new ProgressMonitor.Stub(), sb);\r
-                               } catch (IOException e1) {\r
-                                       // sb cannot throw ioexception\r
-                               }\r
-                       } catch (HistoryException e1) {\r
-                               e1.toString();\r
-                       }\r
-                       Toolkit toolkit = Toolkit.getDefaultToolkit();\r
-                       Clipboard clipboard = toolkit.getSystemClipboard();\r
-                       StringSelection strSel = new StringSelection(sb.toString());\r
-                       clipboard.setContents(strSel, null);\r
-               }\r
-               return false;\r
-       }\r
-        \r
-    static class Grab {\r
-        int mouseButton;\r
-       IMouseCursorHandle cursor;\r
-       Point2D.Double mousepos = new Point2D.Double();\r
-       boolean plot = false;\r
-       boolean horizRuler = false;\r
-       boolean vert = false;\r
-       boolean horiz = false;\r
-       int vertRuler = -1;\r
-       double sx = 1;\r
-       double[] sy;\r
-        boolean valueTipGrab = false;\r
-        boolean valueBoxGrab = false;\r
-        Point2D valueBoxRelPos = new Point2D.Double(); \r
-        Rectangle2D valueBoxPos = new Rectangle2D.Double();\r
-    }\r
-\r
-    private static boolean draggingValueTip(Grab g) {\r
-        return g != null && g.valueTipGrab;\r
-    }\r
-\r
-    private static boolean draggingValueBox(Grab g) {\r
-        return g != null && g.valueBoxGrab;\r
-    }\r
-\r
-}\r
+/*******************************************************************************
+ * Copyright (c) 2007, 2011 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * 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
+ *
+ * Contributors:
+ *     VTT Technical Research Centre of Finland - initial API and implementation
+ *******************************************************************************/
+package org.simantics.trend.impl;
+
+import java.awt.Cursor;
+import java.awt.Toolkit;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.StringSelection;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.print.PrinterException;
+import java.io.File;
+import java.io.IOException;
+
+import org.simantics.databoard.accessor.error.AccessorException;
+import org.simantics.databoard.util.Bean;
+import org.simantics.g2d.canvas.Hints;
+import org.simantics.g2d.canvas.ICanvasContext;
+import org.simantics.g2d.canvas.IMouseCursorHandle;
+import org.simantics.g2d.canvas.impl.AbstractCanvasParticipant;
+import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
+import org.simantics.g2d.canvas.impl.HintReflection.HintListener;
+import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
+import org.simantics.g2d.chassis.ITooltipProvider;
+import org.simantics.g2d.participant.TimeParticipant;
+import org.simantics.history.HistoryException;
+import org.simantics.history.csv.CSVFormatter;
+import org.simantics.history.util.ProgressMonitor;
+import org.simantics.scenegraph.g2d.IG2DNode;
+import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;
+import org.simantics.scenegraph.g2d.events.MouseEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseExitEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseWheelMovedEvent;
+import org.simantics.scenegraph.g2d.events.TimeEvent;
+import org.simantics.scenegraph.g2d.events.command.CommandEvent;
+import org.simantics.scenegraph.g2d.events.command.Commands;
+import org.simantics.trend.configuration.ItemPlacement;
+import org.simantics.trend.configuration.TrendItem.Renderer;
+import org.simantics.trend.util.PrintUtil;
+import org.simantics.utils.datastructures.hints.IHintContext.Key;
+import org.simantics.utils.datastructures.hints.IHintContext.KeyOf;
+import org.simantics.utils.datastructures.hints.IHintObservable;
+
+public class TrendParticipant extends AbstractCanvasParticipant {
+
+    /**
+     * Key for storing chart value tip box relative position. Changed by
+     * {@link TrendParticipant} after user finishes dragging this class around.
+     * */
+    public static final Key KEY_VALUE_TIP_BOX_RELATIVE_POS = new KeyOf(Point2D.class);
+
+    /** Key for chart redraw interval in milliseconds (drawn only if dirty) */
+    public static final Key KEY_TREND_DRAW_INTERVAL = new KeyOf(Long.class);
+
+    /** The time code when trend was last redrawn */
+    public static final Key KEY_TREND_SHAPE_LAST = new KeyOf(Long.class);
+
+    /** Key for interval time of value scale */
+    public static final Key KEY_TREND_AUTOSCALE_INTERVAL = new KeyOf(Long.class);
+
+    /** Key for time of last autoscale */
+    public static final Key KEY_TREND_AUTOSCALE_LAST = new KeyOf(Long.class);
+
+    public static final double KEY_MOVE = 0.33;
+
+    /** Cursor when panning */
+    public MouseCursors cursors = new MouseCursors();
+
+    @HintListener(Class=Hints.class, Field="KEY_CONTROL_BOUNDS")
+    public void selectionChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
+       trend.shapedirty = true;
+       setDirty();
+    }
+
+    TrendNode trend;
+
+    @Dependency TimeParticipant time;
+
+    Grab grab; 
+
+    // The single item on which the mouse hovers over is set to this field. (Binary items are selectable). 
+    public ItemNode hoveringItem;
+
+    @Override
+    public void addedToContext(ICanvasContext ctx) {
+        super.addedToContext(ctx);
+        time.registerForEvents(getClass());
+    }
+
+    public void setTrend(TrendNode node) {
+        trend = node;
+    }
+
+    public TrendNode getTrend() {
+        return trend;
+    }
+
+//    @SGInit
+//    public void initSG(G2DParentNode parent) {
+//     Set<TrendNode> nodes = NodeUtil.collectNodes(parent, TrendNode.class);
+//     trends.addAll(nodes);
+//    }
+
+    @SGCleanup
+    public void cleanupSG() {
+    }
+
+    protected void updateNode() {
+//     titleNode.setText(text)
+//        node.setEnabled(isPaintingEnabled());
+//        node.setGridColor(getGridColor());
+//        node.setGridSize(getGridSize());
+    }
+
+    @EventHandler(priority = 0)
+    public boolean handleTimeEvent(TimeEvent e) {
+        //System.out.println("(" + isRemoved() + ") time event: " + e.time + " (" + e.interval + ")");
+        if (isRemoved()) {
+            if (time != null)
+                time.unregisterForEvents(getClass());
+            return false;
+        }
+
+        long currentTime = e.time;
+
+        Long drawInterval = getHint(KEY_TREND_DRAW_INTERVAL);
+        if (drawInterval == null) drawInterval = 200L;
+        Long lastDrawTime = getHint(KEY_TREND_SHAPE_LAST);
+        boolean drawIntervalElapsed = lastDrawTime == null || (currentTime>=lastDrawTime+drawInterval);
+        if (drawIntervalElapsed) {
+//            System.out.println("elapsed="+drawIntervalElapsed+" currentTime="+currentTime+" lastDraw="+lastDrawTime+", interval="+drawInterval+", nextDrawTime="+(lastDrawTime+drawInterval));
+//            System.out.println("elapsed="+drawIntervalElapsed+" currentTime="+currentTime+" lastDraw="+lastDrawTime+", interval="+drawInterval);
+            setHint(KEY_TREND_SHAPE_LAST, currentTime);
+            trend.shapedirty |= trend.datadirty; 
+            trend.datadirty = false;
+        }
+
+        // Scale time
+        Long autoscaleInterval = getHint(KEY_TREND_AUTOSCALE_INTERVAL);
+        if (autoscaleInterval == null) autoscaleInterval = 1000L;
+        Long autoscaleTime = getHint(KEY_TREND_AUTOSCALE_LAST);
+        boolean autoscale = autoscaleTime==null || (currentTime>=autoscaleTime+autoscaleInterval);
+        if (autoscale) {
+            boolean l = trend.autoscale(trend.autoscaletime, autoscale);
+            if (!draggingValueTip(grab)) {
+                trend.updateValueTipTime();
+            }
+            if (l) {
+                trend.layout();
+            }
+
+            setHint(KEY_TREND_AUTOSCALE_LAST, currentTime);
+        }
+
+        // IT IS DRAW TIME
+        if (trend.shapedirty) {
+//            System.out.println(currentTime+": Draw time!");
+            setDirty();
+        }
+
+        return false;
+    }
+
+    @EventHandler(priority = 0)
+    public boolean mouseClick(MouseClickEvent me) {
+               IG2DNode node = trend.pickNode( me.controlPosition );
+               
+       if (me.clickCount==2 && node!=null) {
+               trend.valueTipTime = trend.prevValueTipTime;
+                       if (node instanceof Plot) {
+                               boolean binaryArea = me.controlPosition.getY()>trend.plot.getY()+trend.plot.analogAreaHeight;
+                               if (binaryArea) {
+                                       trend.horizRuler.zoomOut();
+                               } else {
+                                       trend.zoomOut();
+                               }
+                               setDirty();
+                               return true;
+                       } else if (node instanceof HorizRuler) {
+                               HorizRuler hr = (HorizRuler) node;
+                               hr.zoomOut();
+                               setDirty();
+                               return true;
+                       } else if (node instanceof VertRuler) {
+                               VertRuler vr = (VertRuler) node;
+                               vr.zoomOut();
+                               setDirty();
+                               return true;
+                       }
+       }
+       
+       // Click ValueTip 
+       if (me.clickCount==1 && me.button==MouseEvent.LEFT_BUTTON && node!=null && node instanceof Plot) {
+               trend.prevValueTipTime = trend.valueTipTime;
+                       if (trend.valueTipTime == null) {
+                       trend.valueTipTime = Double.isNaN(trend.mouseHoverTime)?null:trend.mouseHoverTime;
+                       trend.valueTipHover = true;
+                       } else {
+                               double valueTipX = trend.horizRuler.toX( trend.valueTipTime );
+                       double x = me.controlPosition.getX() - trend.horizRuler.getBounds().getX();
+                       boolean hit = x>=valueTipX-TrendLayout.VALUE_TIP_HOVER_SENSITIVITY && x<=valueTipX+TrendLayout.VALUE_TIP_HOVER_SENSITIVITY;
+                       if (hit) trend.valueTipTime = null;
+                       }
+                       setDirty();
+       }
+
+       /*
+       // Right click removes value tip
+       if (me.clickCount==1 && me.button==MouseEvent.RIGHT_BUTTON) {
+                       if (trend.valueTipTime != null) {
+                               trend.valueTipTime = null;
+                               setDirty();
+                       }               
+       }*/
+
+       if (me.clickCount==1 && me.button==MouseEvent.LEFT_BUTTON) {
+                       if (node instanceof VertRuler) {
+                               VertRuler vr = (VertRuler) node;
+                               trend.selectVertRuler( trend.vertRulers.indexOf(vr) );
+                               setDirty();
+                       } else if (node == null || node instanceof TextNode) {
+                               Plot p = trend.plot;
+                               double x = me.controlPosition.getX() - p.getX();
+                               double y = me.controlPosition.getY() - p.getY();
+                               if ( x>=0 && x<=p.getWidth() && y<=0 && y>=-Plot.DIAMOND_SIZE*2) {
+                                       // Click hits milestone area
+                                       milestoneSearch: for ( Milestone ms : trend.milestones.milestones ) {
+                                               double mx = trend.horizRuler.toX( ms.time );
+                                               if ( x>=mx-Plot.DIAMOND_SIZE && x<=mx+Plot.DIAMOND_SIZE ) {
+                                                       ITooltipProvider tp = getContext().getTooltipProvider();
+                                                       if (tp!=null) {
+                                                               tp.show(ms.label, ms.description);
+                                                       }
+                                                       break milestoneSearch;
+                                               }
+                                       }
+                               }
+
+                       }
+
+       }
+               return false;
+    }
+
+    @EventHandler(priority = 0)
+    public boolean mouseDown(MouseButtonPressedEvent me) {
+       // Start Box-Zoom
+       if (me.button==MouseEvent.LEFT_BUTTON) {
+                       IG2DNode node = trend.pickNode( me.controlPosition );   
+                       if (node == null || node instanceof Plot) {
+
+                // Start value box grab
+                {
+                    double x = me.controlPosition.getX() - trend.horizRuler.getBounds().getX();
+                    double y = me.controlPosition.getY() - trend.plot.getBounds().getY();
+                    if (trend.plot.valueTipBoxBounds.contains(x, y)) {
+                        //System.out.println("grabbed value box @ " + x + ", " + y);
+                        grab = new Grab();
+                        grab.valueBoxRelPos.setLocation(trend.spec.viewProfile.valueViewPositionX, trend.spec.viewProfile.valueViewPositionY);
+                        grab.valueBoxPos.setFrame(trend.plot.valueTipBoxBounds);
+                        grab.mousepos = new Point2D.Double( me.controlPosition.getX(), me.controlPosition.getY() );
+                        grab.valueBoxGrab = true;
+                        return true;
+                    }
+                }
+
+                               // Start Value-Tip grab
+                               if (trend.valueTipTime!=null) {
+                                       double valueTipX = trend.horizRuler.toX( trend.valueTipTime );
+                               double x = me.controlPosition.getX() - trend.horizRuler.getBounds().getX();
+                               boolean hit = x>=valueTipX-TrendLayout.VALUE_TIP_HOVER_SENSITIVITY && x<=valueTipX+TrendLayout.VALUE_TIP_HOVER_SENSITIVITY; 
+                               if (hit) {
+                                       grab = new Grab();
+                                       grab.mousepos = new Point2D.Double( me.controlPosition.getX(), me.controlPosition.getY() );
+                                       grab.sx = trend.horizRuler.unitsPerPixel();
+                                       grab.sy = new double[ trend.vertRulers.size() ];
+                                       grab.valueTipGrab = true;
+                                       return true;
+                               }
+                               }
+
+                       // Start Box-Zoom
+                               boolean binaryArea = me.controlPosition.getY()>trend.plot.getY()+trend.plot.analogAreaHeight;
+                               boolean timeZoom = (me.stateMask & MouseEvent.SHIFT_MASK)>0 || binaryArea;
+                               if (trend.selection==null) {
+                                       trend.selection = trend.addNode("Selection", SelectionNode.class);
+                                       trend.selection.setZIndex( 10 );
+                                       trend.selection.start( me.controlPosition, binaryArea, timeZoom );
+                               }
+                       return true;
+                       }
+       }
+
+       // Start grab
+               IG2DNode node = trend.pickNode( me.controlPosition );
+       if ( (me.button==MouseEvent.MIDDLE_BUTTON) && 
+                       node!=null && node instanceof Plot) {
+                       //Plot p = (Plot) node;
+                       TrendNode trend = (TrendNode) node.getParent();
+                       boolean shift = (me.stateMask & MouseEvent.SHIFT_MASK)>0;
+                       boolean alt = (me.stateMask & MouseEvent.ALT_MASK)>0;
+                       //boolean analogArea = me.controlPosition.getY() < p.getY()+p.analogAreaHeight;
+                       //boolean binaryArea = me.controlPosition.getY() >= p.getY()+p.analogAreaHeight;
+               grab = new Grab();
+               grab.mousepos = new Point2D.Double( me.controlPosition.getX(), me.controlPosition.getY() );
+               grab.sx = trend.horizRuler.unitsPerPixel();
+               grab.sy = new double[ trend.vertRulers.size() ];
+               grab.horiz = !shift || alt;
+               grab.vert = shift || alt;
+               if (grab.vert) {
+                       for (int i=0; i<trend.vertRulers.size(); i++)
+                               grab.sy[i] = trend.vertRulers.get(i).unitsPerPixel();
+               }
+               Cursor c = grab.horiz ? (grab.vert ? cursors.grab : cursors.grab_horiz) : (grab.vert ? cursors.grab_vert : cursors.grab); 
+               grab.cursor = getContext().getMouseCursorContext().setCursor(me.mouseId, c);
+               grab.mouseButton = me.button;
+               grab.plot = true;
+                       trend.horizRuler.translate(0);
+                       setHoverTime(null);
+               setDirty();
+                       return true;
+       }
+               if ( (me.button==MouseEvent.LEFT_BUTTON||me.button==MouseEvent.MIDDLE_BUTTON) && 
+                               node!=null && node instanceof HorizRuler) {
+                       HorizRuler hr = (HorizRuler) node;
+               grab = new Grab();
+               grab.cursor = getContext().getMouseCursorContext().setCursor(me.mouseId, cursors.grab_horiz);
+               grab.mousepos = new Point2D.Double();
+               grab.mousepos.setLocation( me.controlPosition );
+               grab.sx = hr.unitsPerPixel();
+               grab.horizRuler = true;
+               grab.mouseButton = me.button;
+               grab.horiz = true;
+                       trend.horizRuler.translate(0);
+               setDirty();
+                       return true;
+               }
+               if ( (me.button==MouseEvent.LEFT_BUTTON||me.button==MouseEvent.MIDDLE_BUTTON) && 
+                               node!=null && node instanceof VertRuler) {
+                       //VertRuler vr = (VertRuler) node;
+               grab = new Grab();
+               grab.cursor = getContext().getMouseCursorContext().setCursor(me.mouseId, cursors.grab_vert);
+               grab.mousepos = new Point2D.Double();
+               grab.mousepos.setLocation( me.controlPosition );
+               grab.sy = new double[ trend.vertRulers.size() ];
+               for (int i=0; i<trend.vertRulers.size(); i++)
+                       grab.sy[i] = trend.vertRulers.get(i).unitsPerPixel();
+               grab.mouseButton = me.button;
+               grab.vertRuler = trend.vertRulers.indexOf(node);
+                       trend.selectVertRuler( grab.vertRuler );
+               setDirty();
+                       return true;
+       }
+       
+       return false;
+    }
+
+    @EventHandler(priority = 0)
+    public boolean mouseUp(MouseButtonReleasedEvent me) {
+
+        // Release value box grab
+        if (me.button==1 && draggingValueBox(grab)) {
+            setHint(KEY_VALUE_TIP_BOX_RELATIVE_POS, new Point2D.Double(
+                    trend.spec.viewProfile.valueViewPositionX,
+                    trend.spec.viewProfile.valueViewPositionY));
+            grab = null;
+        }
+
+        // Release value tip grab
+        if (me.button==1 && draggingValueTip(grab)) {
+            grab = null;
+        }
+
+       // Release Box-Zoom
+       if (me.button==1 && trend.selection!=null) {
+               Rectangle2D rect = trend.selection.rect;
+               
+               if (rect.getWidth()>1 && rect.getHeight()>1) {    
+                       if (trend.selection.timeZoom) {
+                               trend.horizRuler.zoomIn( rect.getX()-trend.horizRuler.getX(), rect.getWidth() );
+                       } else {
+                               trend.zoomIn( 
+                                               rect.getX()-trend.plot.getX(), 
+                                               rect.getY()-trend.plot.getY(),
+                                               rect.getWidth(), rect.getHeight(), true, true );
+                       }
+                       trend.layout();
+                               trend.shapedirty = true;
+                       setDirty();
+               }
+               trend.selection.delete();
+               trend.selection = null;
+
+               return true;
+       }
+       
+       if (grab!=null && grab.mouseButton==me.button) {
+               grab.cursor.remove();
+               grab = null;
+       }
+       return false;
+    }
+
+    @EventHandler(priority = 0)
+    public boolean mouseExit(MouseExitEvent me) {
+               trend.valueTipHover = false;
+       setHoverTime( null );
+       return false;
+    }
+
+    void setHoverTime(Double time) {
+       if ( time==null && Double.isNaN(trend.mouseHoverTime) ) return;
+       if ( time!=null && trend.mouseHoverTime==time ) return;
+       if ( time==null ) {
+                       trend.mouseHoverTime = Double.NaN;
+       } else {
+               trend.mouseHoverTime = time;
+               trend.lastMouseHoverTime = time;
+       }
+       setDirty();
+    }
+
+//    @EventHandler(priority = 0)
+//    public boolean mouseEnter(MouseExitEvent me) {
+//     //double x = me.controlPosition.getX() - trend.horizRuler.getBounds().getX();
+//     //double time = trend.horizRuler.toTime( x ) - trend.horizRuler.basetime;
+//     return false;
+//    }
+
+    @SuppressWarnings("unused")
+    @EventHandler(priority = 0)
+    public boolean mouseMoved(MouseMovedEvent me) {
+//        ITooltipProvider tt = getContext().getTooltipProvider();
+//        if (tt!=null) {
+//            tt.setTooltipText("Hello");
+//        }
+
+               IG2DNode pick = trend.pickNode( me.controlPosition );
+       
+               TrackMouseMovement: {
+                       if ( pick == trend.plot ) {
+                       double x = me.controlPosition.getX() - trend.horizRuler.getBounds().getX();
+                       double time = trend.horizRuler.toTime( x );
+                       double sx = (trend.horizRuler.end-trend.horizRuler.from) / trend.horizRuler.getWidth();
+                       double timeSnapTolerance = sx * 7.0; 
+                               boolean shift = (me.stateMask & MouseEvent.SHIFT_MASK)>0;
+                       
+                               // SNAP - When shift is held
+                       if (shift) {
+                               Double snappedTime = null;
+                                       try {
+                                               snappedTime = trend.snapToValue( time, timeSnapTolerance );
+                                               if (snappedTime != null) time = snappedTime;
+                                       } catch (HistoryException e) {
+                                       } catch (AccessorException e) {
+                                       }
+                       }
+                               
+                               setHoverTime(time);
+                       }
+               }
+
+        // Implement value box moving while dragging.
+        if (draggingValueBox(grab)) {
+            Rectangle2D plotBox = trend.plot.getBounds();
+            Rectangle2D valueBoxPos = grab.valueBoxPos;
+            Rectangle2D valueBox = trend.plot.valueTipBoxBounds;
+            double dx = me.controlPosition.getX() - grab.mousepos.getX();
+            double dy = me.controlPosition.getY() - grab.mousepos.getY();
+            double margin = Plot.VALUE_TIP_BOX_PLOT_MARGIN;
+            double maxX = plotBox.getWidth() - margin - valueBox.getWidth();
+            double maxY = plotBox.getHeight() - margin - valueBox.getHeight();
+            double boxX = valueBoxPos.getX() + dx;
+            double boxY = valueBoxPos.getY() + dy;
+            double pw = plotBox.getWidth() - 2 * margin;
+            double ph = plotBox.getHeight() - 2 * margin;
+            double w = pw - valueBox.getWidth();
+            double h = ph - valueBox.getHeight();
+            if (w > 0 && h > 0) {
+                double rx = (boxX - margin) / w;
+                double ry = (boxY - margin) / h;
+                rx = Math.max(0, Math.min(rx, 1));
+                ry = Math.max(0, Math.min(ry, 1));
+                trend.spec.viewProfile.valueViewPositionX = rx;
+                trend.spec.viewProfile.valueViewPositionY = ry;
+                setDirty();
+            }
+            return false;
+        }
+
+        ValueToolTip: if ( pick == trend.plot) {
+            if (draggingValueTip(grab)) {
+                               // Move grabbed value-tip
+                               trend.valueTipTime = Double.isNaN(trend.mouseHoverTime) ? null : trend.mouseHoverTime;
+                               return false;
+                       } else {
+                               // Track hover color
+                               if (trend.valueTipTime!=null) {
+                                       double valueTipX = trend.horizRuler.toX( trend.valueTipTime );
+                               double x = me.controlPosition.getX() - trend.horizRuler.getBounds().getX();
+                               boolean hit = x>=valueTipX-TrendLayout.VALUE_TIP_HOVER_SENSITIVITY && x<=valueTipX+TrendLayout.VALUE_TIP_HOVER_SENSITIVITY;
+                               trend.valueTipHover = hit;
+                               }
+                       }
+       }
+               
+               // Item pick
+               ItemPick: {
+               if ( pick instanceof VertRuler ) {
+                       VertRuler vr = (VertRuler) pick;
+                       hoveringItem = null;
+                       for (ItemNode item : trend.analogItems) {
+                               if (item.item.renderer != Renderer.Analog || item.ruler==vr) {
+                                       if ( hoveringItem != null) break;
+                                       hoveringItem = item;
+                               }
+                       }
+               } else if ( pick == null || pick instanceof Plot ) {
+                       hoveringItem = trend.plot.pickItem( me.controlPosition );
+               } else {
+                       hoveringItem = null;
+               }
+       }
+       
+       // Box-Zoom
+       if (trend.selection!=null) {
+               trend.selection.setEndPoint( me.controlPosition );
+               setHoverTime(null);
+               setDirty();
+               return true;
+       }
+       
+       // Drag axis
+       if (grab != null) {
+               double dx = me.controlPosition.getX() - grab.mousepos.x;
+               double dy = me.controlPosition.getY() - grab.mousepos.y;
+               grab.mousepos.setLocation(me.controlPosition);
+               
+               if (grab.plot) {
+                       if (grab.horiz) {
+                               trend.horizRuler.translate(-dx*grab.sx);
+                       }
+                       if (grab.vert) {
+                               for (int i=0; i<grab.sy.length; i++) {
+                                       if (i>=trend.vertRulers.size()) break;
+                                       trend.vertRulers.get(i).translate(dy*grab.sy[i]);
+                               }
+                       }
+               } else if (grab.horizRuler) {
+                       trend.horizRuler.translate(-dx*grab.sx);
+               } else if (grab.vertRuler>=0) {
+                       if (grab.vertRuler<=trend.vertRulers.size()) {
+                               VertRuler vr = trend.vertRulers.get( grab.vertRuler );
+                               trend.selectVertRuler( grab.vertRuler );
+                               vr.translate(dy*grab.sy[ grab.vertRuler ]);
+                       }
+               }
+                       trend.layout();
+               trend.shapedirty = true;
+               setDirty();
+               return true;
+       }
+       return false;
+    }
+
+    @EventHandler(priority = 0)
+    public boolean mouseWheel(MouseWheelMovedEvent me) {
+               IG2DNode node = trend.pickNode( me.controlPosition );
+               if (node instanceof Plot || node instanceof HorizRuler || node instanceof VertRuler) {
+                       Point2D pt = node.parentToLocal( me.controlPosition );
+                       boolean shift = (me.stateMask & MouseEvent.SHIFT_MASK)>0;
+                       boolean alt = (me.stateMask & MouseEvent.ALT_MASK)>0;
+                       double pw = trend.plot.getWidth();
+                       double ph = trend.plot.analogAreaHeight;
+
+                       double r = Math.pow(0.9, me.wheelRotation);
+
+                       double w = r * pw; 
+                       double h = r * ph;
+                       double rx = pt.getX() / pw;
+                       double ry = pt.getY() / ph;
+
+                       double zx = (pw-w)*rx;
+                       double zy = (ph-h)*ry;
+
+                       if (node instanceof Plot) {
+                               TrendNode trend = (TrendNode) node.getParent();
+                               if (shift||alt) {
+                                       for (VertRuler vr : trend.vertRulers) {
+                                               vr.zoomIn(zy, h);
+                                       }
+                               } 
+                               if (!shift) {
+                                       trend.horizRuler.zoomIn(zx, w);
+                               }
+                       }
+
+                       if (node instanceof HorizRuler) {
+                               //HorizRuler hr = (HorizRuler) node;
+                               trend.horizRuler.zoomIn(zx, w);
+                       }
+
+                       if (node instanceof VertRuler) {
+                               VertRuler vr = (VertRuler) node;
+                               trend.selectVertRuler( trend.vertRulers.indexOf(vr) );
+                               vr.zoomIn(zy, h);
+                       }
+                       trend.shapedirty = true;
+                       trend.layout();
+                       setDirty();
+                       return true;
+               }
+       return false;
+    }
+
+    @EventHandler(priority = 0)
+    public boolean handleCommandEvent(CommandEvent e) {
+       
+       if (e.command == Commands.CANCEL) {
+               if (trend.selection != null) {
+                       trend.selection.delete();
+                       trend.selection = null;
+                       setDirty();
+                       return true;
+               }
+               
+               if (grab!=null) {
+                if (draggingValueBox(grab)) {
+                    trend.spec.viewProfile.valueViewPositionX = grab.valueBoxRelPos.getX();
+                    trend.spec.viewProfile.valueViewPositionY = grab.valueBoxRelPos.getY();
+                }
+                       if (grab.cursor!=null) grab.cursor.remove();
+                       grab = null;
+                       setDirty();
+                       return true;
+               }
+               
+               if (trend.valueTipTime != null) {
+                       trend.valueTipTime = null;
+                       setDirty();
+                       return true;
+               }
+               return false;
+       }
+       
+       if (e.command == Commands.PAN_LEFT) {
+                       double pw = trend.plot.getWidth();
+                       double ph = trend.plot.analogAreaHeight;
+                       double zx = -pw * KEY_MOVE;
+                       double zy = 0;
+                       trend.zoomIn(zx, zy, pw, ph, true, true);
+                       trend.layout();
+                       setDirty();
+       }
+       
+       if (e.command == Commands.PAN_RIGHT) {
+                       double pw = trend.plot.getWidth();
+                       double ph = trend.plot.analogAreaHeight;
+                       double zx = +pw * KEY_MOVE;
+                       double zy = 0;
+                       trend.zoomIn(zx, zy, pw, ph, true, true);
+                       trend.horizRuler.autoscroll = false;
+                       trend.layout();
+                       setDirty();
+       }
+       
+       if (e.command == Commands.PAN_UP) {
+                       double pw = trend.plot.getWidth();
+                       double ph = trend.plot.analogAreaHeight;
+                       double zx = 0;
+                       double zy = -ph * KEY_MOVE;
+                       trend.zoomIn(zx, zy, pw, ph, true, true);
+                       trend.horizRuler.autoscroll = false;
+                       trend.layout();
+                       setDirty();
+       }
+       
+       if (e.command == Commands.PAN_DOWN) {
+                       double pw = trend.plot.getWidth();
+                       double ph = trend.plot.analogAreaHeight;
+                       double zx = 0;
+                       double zy = +ph * KEY_MOVE;
+                       trend.zoomIn(zx, zy, pw, ph, true, true);
+                       trend.horizRuler.autoscroll = false;
+                       trend.layout();
+                       setDirty();
+       }
+       
+       // Zoom out to time window settings (VK_4)
+       if (e.command.equals(Commands.AUTOSCALE)) {
+                       trend.horizRuler.autoscroll = true;
+                       for (VertRuler vertRuler : trend.vertRulers) vertRuler.autoscroll = true;
+                       trend.zoomOut();
+                       trend.layout();
+                       setDirty();
+       }
+       
+       // Fit all visible (VK_1)
+       if (e.command.equals(Commands.ZOOM_TO_FIT)) {
+                       trend.readMinMaxFromEnd();
+                       trend.horizRuler.setFromEnd(trend.horizRuler.iFrom, trend.horizRuler.iEnd);
+                       trend.horizRuler.fireListener();
+                       int c = trend.vertRulers.size();
+                       for (int i=0; i<c; i++) {
+                               VertRuler vr = trend.vertRulers.get(c-i-1);
+                               double nMin = vr.iMin;
+                               double nMax = vr.iMax;
+
+                               double diff = nMax - nMin;
+                               if (diff==0.0) {
+                                       nMin -= 0.5;
+                                       nMax += 0.5;
+                                       diff = nMax - nMin;
+                               }
+                               double margin = diff*0.02;
+                               
+                               if (trend.itemPlacement == ItemPlacement.Stacked) {
+                                       nMin = nMin - (diff)*i - margin;
+                                       nMax = nMax + (diff)*(c-i-1) + margin;
+                                       
+                               } else {
+                                       nMin = vr.iMin - margin;
+                                       nMax = vr.iMax + margin;
+                               }
+                               
+                               vr.zoomTo(nMin, nMax);
+                       }
+                       trend.horizRuler.autoscroll = false;
+                       trend.layout();
+                       setDirty();
+                       return true;
+       }
+
+       // Fit horiz (VK_2)
+       if (e.command.equals(Commands.ZOOM_TO_FIT_HORIZ)) {
+                       trend.readMinMaxFromEnd();
+                       trend.horizRuler.setFromEnd(trend.horizRuler.iFrom, trend.horizRuler.iEnd);
+                       trend.layout();
+                       trend.horizRuler.autoscroll = false;
+                       setDirty();
+                       return true;
+       }
+
+       // Fit vert (VK_3)
+       if (e.command.equals(Commands.ZOOM_TO_FIT_VERT)) {
+                       trend.readMinMaxFromEnd();
+                       int c = trend.vertRulers.size();
+                       for (int i=0; i<c; i++) {
+                               VertRuler vr = trend.vertRulers.get(c-i-1);
+                               double nMin = vr.iMin;
+                               double nMax = vr.iMax;
+
+                               double diff = nMax - nMin;
+                               if (diff==0.0) {
+                                       nMin -= 0.5;
+                                       nMax += 0.5;
+                                       diff = nMax - nMin;
+                               }
+                               double margin = diff*0.02;
+                               
+                               if (trend.itemPlacement == ItemPlacement.Stacked) {
+                                       nMin = nMin - (diff)*i - margin;
+                                       nMax = nMax + (diff)*(c-i-1) + margin;
+                                       
+                               } else {
+                                       nMin = vr.iMin - margin;
+                                       nMax = vr.iMax + margin;
+                               }
+                               
+                               vr.zoomTo(nMin, nMax);
+                       }
+                       trend.layout();
+                       setDirty();
+                       return true;
+       }
+       
+       // +
+       if (e.command == Commands.ZOOM_IN) {
+                       double pw = trend.plot.getWidth();
+                       double ph = trend.plot.analogAreaHeight;
+                       double za = 3; // Zoom amount
+                       double w = (1. - (0.1*za)) * pw; 
+                       double h = (1. - (0.1*za)) * ph;
+
+                       double zx = (pw-w)/2;
+                       double zy = (ph-h)/2;
+                       
+                       trend.zoomIn(zx, zy, w, h, true, true);
+                       trend.horizRuler.autoscroll = false;
+                       trend.layout();
+                       setDirty();
+                       return true;
+       }
+       
+       // -
+       if (e.command == Commands.ZOOM_OUT) {
+                       double pw = trend.plot.getWidth();
+                       double ph = trend.plot.analogAreaHeight;
+                       double za = -3; // Zoom amount
+                       double w = (1. - (0.1*za)) * pw; 
+                       double h = (1. - (0.1*za)) * ph;
+
+                       double zx = (pw-w)/2;
+                       double zy = (ph-h)/2;
+                       
+                       trend.zoomIn(zx, zy, w, h, true, true);
+                       trend.horizRuler.autoscroll = false;
+                       trend.layout();
+                       setDirty();
+                       return true;
+       }
+       
+               // Print to printer
+               if (e.command == Commands.PRINT) {
+                       try {
+                               PrintUtil pu = new PrintUtil();
+                               pu.addTrendPage(trend);
+                               pu.print();
+                       } catch (PrinterException e1) {
+                               e1.printStackTrace();
+                       }
+                       return true;
+               }
+               
+               // Print to PDF
+               if (e.command == Commands.PDFPRINT) {
+                       PrintUtil pu = new PrintUtil();
+                       pu.addTrendPage(trend);
+                       try {
+                               File f = File.createTempFile("Trend", ".pdf");
+                               pu.printPdf(f);
+                               System.out.println("Printed Trend to "+f);
+                       } catch (IOException e1) {
+                               e1.printStackTrace();
+                       }
+                       return true;
+               }
+               
+               // Read visible values into CSV 
+               if (e.command == Commands.COPY || e.command == Commands.EXPORT) {
+                       StringBuilder sb = new StringBuilder(64*1024);
+                       try {
+                               // TODO: fix to use preferences.
+                               CSVFormatter formatter = new CSVFormatter();
+                               formatter.setTimeRange(trend.horizRuler.from, trend.horizRuler.end);
+                               for (ItemNode i : trend.allItems)
+                               {
+                                       if (i.item.hidden) continue;
+                                       if (i.historyItems==null || i.historyItems.length==0) continue;
+                                       Bean bestQualityStream = i.historyItems[0];
+                                       String historyItemId = (String) bestQualityStream.getFieldUnchecked("id");
+                                       formatter.addItem( trend.historian, historyItemId, i.item.simpleLabel, i.item.variableReference, i.item.unit);
+                               }
+                               formatter.sort();
+                               try {
+                                   // TODO: Use a proper user-cancelable progressmonitor
+                                       formatter.formulate2(new ProgressMonitor.Stub(), sb);
+                               } catch (IOException e1) {
+                                       // sb cannot throw ioexception
+                               }
+                       } catch (HistoryException e1) {
+                               e1.toString();
+                       }
+                       Toolkit toolkit = Toolkit.getDefaultToolkit();
+                       Clipboard clipboard = toolkit.getSystemClipboard();
+                       StringSelection strSel = new StringSelection(sb.toString());
+                       clipboard.setContents(strSel, null);
+               }
+               return false;
+       }
+        
+    static class Grab {
+        int mouseButton;
+       IMouseCursorHandle cursor;
+       Point2D.Double mousepos = new Point2D.Double();
+       boolean plot = false;
+       boolean horizRuler = false;
+       boolean vert = false;
+       boolean horiz = false;
+       int vertRuler = -1;
+       double sx = 1;
+       double[] sy;
+        boolean valueTipGrab = false;
+        boolean valueBoxGrab = false;
+        Point2D valueBoxRelPos = new Point2D.Double(); 
+        Rectangle2D valueBoxPos = new Rectangle2D.Double();
+    }
+
+    private static boolean draggingValueTip(Grab g) {
+        return g != null && g.valueTipGrab;
+    }
+
+    private static boolean draggingValueBox(Grab g) {
+        return g != null && g.valueBoxGrab;
+    }
+
+}