/******************************************************************************* * 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 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; i1 && 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=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