X-Git-Url: https://gerrit.simantics.org/r/gitweb?p=simantics%2Fplatform.git;a=blobdiff_plain;f=bundles%2Forg.simantics.charts%2Fsrc%2Forg%2Fsimantics%2Fcharts%2Feditor%2FTimeSeriesEditor.java;fp=bundles%2Forg.simantics.charts%2Fsrc%2Forg%2Fsimantics%2Fcharts%2Feditor%2FTimeSeriesEditor.java;h=4377eddc6076782c1997ea766618506e1f23c1e9;hp=0000000000000000000000000000000000000000;hb=969bd23cab98a79ca9101af33334000879fb60c5;hpb=866dba5cd5a3929bbeae85991796acb212338a08 diff --git a/bundles/org.simantics.charts/src/org/simantics/charts/editor/TimeSeriesEditor.java b/bundles/org.simantics.charts/src/org/simantics/charts/editor/TimeSeriesEditor.java new file mode 100644 index 000000000..4377eddc6 --- /dev/null +++ b/bundles/org.simantics.charts/src/org/simantics/charts/editor/TimeSeriesEditor.java @@ -0,0 +1,1048 @@ +/******************************************************************************* + * 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.charts.editor; + +import java.awt.geom.Point2D; +import java.util.Collections; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.core.commands.Command; +import org.eclipse.core.commands.IStateListener; +import org.eclipse.core.commands.State; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; +import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.jface.action.IMenuListener; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorSite; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.commands.ICommandService; +import org.eclipse.ui.contexts.IContextService; +import org.simantics.Simantics; +import org.simantics.browsing.ui.model.browsecontexts.BrowseContext; +import org.simantics.charts.Activator; +import org.simantics.charts.ITrendSupport; +import org.simantics.charts.ontology.ChartResource; +import org.simantics.charts.preference.ChartPreferences; +import org.simantics.charts.query.FindChartItemForTrendItem; +import org.simantics.charts.query.MilestoneSpecQuery; +import org.simantics.charts.query.SetProperty; +import org.simantics.charts.query.TrendSpecQuery; +import org.simantics.charts.ui.ChartLinkData; +import org.simantics.charts.ui.LinkTimeHandler; +import org.simantics.databoard.Bindings; +import org.simantics.databoard.util.ObjectUtils; +import org.simantics.db.AsyncReadGraph; +import org.simantics.db.ReadGraph; +import org.simantics.db.Resource; +import org.simantics.db.Session; +import org.simantics.db.common.request.ParametrizedRead; +import org.simantics.db.common.request.UniqueRead; +import org.simantics.db.exception.DatabaseException; +import org.simantics.db.layer0.request.Model; +import org.simantics.db.layer0.request.combinations.Combinators; +import org.simantics.db.layer0.variable.RVI; +import org.simantics.db.layer0.variable.RVIBuilder; +import org.simantics.db.layer0.variable.Variable; +import org.simantics.db.layer0.variable.Variables; +import org.simantics.db.layer0.variable.Variables.Role; +import org.simantics.db.procedure.AsyncListener; +import org.simantics.db.procedure.SyncListener; +import org.simantics.diagram.participant.ContextUtil; +import org.simantics.diagram.participant.SGFocusParticipant; +import org.simantics.g2d.canvas.ICanvasContext; +import org.simantics.g2d.canvas.impl.CanvasContext; +import org.simantics.g2d.chassis.AWTChassis; +import org.simantics.g2d.chassis.ICanvasChassis; +import org.simantics.g2d.chassis.IChassisListener; +import org.simantics.g2d.chassis.SWTChassis; +import org.simantics.g2d.participant.KeyToCommand; +import org.simantics.g2d.participant.TimeParticipant; +import org.simantics.g2d.utils.CanvasUtils; +import org.simantics.history.Collector; +import org.simantics.history.HistoryManager; +import org.simantics.history.impl.FileHistory; +import org.simantics.project.IProject; +import org.simantics.scenegraph.INode; +import org.simantics.scenegraph.g2d.events.Event; +import org.simantics.scenegraph.g2d.events.EventTypes; +import org.simantics.scenegraph.g2d.events.IEventHandler; +import org.simantics.scenegraph.g2d.events.MouseEvent; +import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent; +import org.simantics.scenegraph.g2d.events.command.Commands; +import org.simantics.selectionview.StandardPropertyPage; +import org.simantics.simulation.data.Datasource; +import org.simantics.simulation.experiment.ExperimentState; +import org.simantics.simulation.experiment.IExperiment; +import org.simantics.simulation.experiment.IExperimentListener; +import org.simantics.simulation.ontology.SimulationResource; +import org.simantics.trend.TrendInitializer; +import org.simantics.trend.TrendInitializer.StepListener; +import org.simantics.trend.configuration.ItemPlacement; +import org.simantics.trend.configuration.LineQuality; +import org.simantics.trend.configuration.TimeFormat; +import org.simantics.trend.configuration.TrendSpec; +import org.simantics.trend.impl.HorizRuler; +import org.simantics.trend.impl.ItemNode; +import org.simantics.trend.impl.MilestoneSpec; +import org.simantics.trend.impl.TrendNode; +import org.simantics.trend.impl.TrendParticipant; +import org.simantics.ui.workbench.IPropertyPage; +import org.simantics.ui.workbench.IResourceEditorInput; +import org.simantics.ui.workbench.ResourceEditorInput; +import org.simantics.ui.workbench.ResourceEditorPart; +import org.simantics.ui.workbench.action.PerformDefaultAction; +import org.simantics.ui.workbench.editor.input.InputValidationCombinators; +import org.simantics.utils.datastructures.hints.HintListenerAdapter; +import org.simantics.utils.datastructures.hints.IHintContext; +import org.simantics.utils.datastructures.hints.IHintContext.Key; +import org.simantics.utils.datastructures.hints.IHintObservable; +import org.simantics.utils.format.ValueFormat; +import org.simantics.utils.threads.AWTThread; +import org.simantics.utils.threads.IThreadWorkQueue; +import org.simantics.utils.threads.SWTThread; +import org.simantics.utils.threads.ThreadUtils; +import org.simantics.utils.ui.BundleUtils; +import org.simantics.utils.ui.ErrorLogger; +import org.simantics.utils.ui.ExceptionUtils; +import org.simantics.utils.ui.SWTUtils; +import org.simantics.utils.ui.dialogs.ShowMessage; +import org.simantics.utils.ui.jface.ActiveSelectionProvider; + +/** + * TimeSeriesEditor is an interactive part that draws a time series chart. + * + * The configuration model is {@link TrendSpec} which is read through + * {@link TrendSpecQuery}. In Simantics Environment the + * editor input is {@link ResourceEditorInput}. + * + * @author Toni Kalajainen + * @author Tuukka Lehtonen + */ +public class TimeSeriesEditor extends ResourceEditorPart { + + ParametrizedRead INPUT_VALIDATOR = + Combinators.compose( + InputValidationCombinators.hasURI(), + InputValidationCombinators.extractInputResource() + ); + + @Override + protected ParametrizedRead getInputValidator() { + return INPUT_VALIDATOR; + } + + /** + * The root property browse context of the time series editor. A transitive + * closure is calculated for this context. + */ + private static String ROOT_PROPERTY_BROWSE_CONTEXT = ChartResource.URIs.ChartBrowseContext; + + /** + * ID of the this editor part extension. + */ + public static final String ID = "org.simantics.charts.editor.timeseries"; + + private static final String CONTEXT_MENU_ID = "#timeSeriesChart"; + + private IEclipsePreferences chartPreferenceNode; + + private final ImageDescriptor IMG_ZOOM_TO_FIT = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/horizAndVert16.png"); + private final ImageDescriptor IMG_ZOOM_TO_FIT_HORIZ = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/horiz16.png"); + private final ImageDescriptor IMG_ZOOM_TO_FIT_VERT = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/vert16.png"); + private final ImageDescriptor IMG_AUTOSCALE = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/autoscale16.png"); + + IPreferenceChangeListener preferenceListener = new IPreferenceChangeListener() { + @Override + public void preferenceChange(PreferenceChangeEvent event) { + if (disposed) { + System.err.println("Warning: pref change to disposed TimeSeriesEditor"); + return; + } + + if ( event.getKey().equals(ChartPreferences.P_REDRAW_INTERVAL ) || + event.getKey().equals(ChartPreferences.P_AUTOSCALE_INTERVAL )) { + long redraw_interval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL); + long autoscale_interval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL); + setInterval( redraw_interval, autoscale_interval ); + } + if ( event.getKey().equals(ChartPreferences.P_DRAW_SAMPLES )) { + boolean draw_samples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES); + setDrawSamples( draw_samples ); + } + if ( event.getKey().equals(ChartPreferences.P_TIMEFORMAT ) ) { + String s = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT); + TimeFormat tf = TimeFormat.valueOf( s ); + if (tf!=null) setTimeFormat( tf ); + } + if ( event.getKey().equals(ChartPreferences.P_VALUEFORMAT ) ) { + String s = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT); + ValueFormat vf = ValueFormat.valueOf( s ); + if (vf!=null) setValueFormat( vf ); + } + if ( event.getKey().equals(ChartPreferences.P_ITEMPLACEMENT)) { + String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT); + ItemPlacement ip = ItemPlacement.valueOf(s); + if (trendNode!=null) trendNode.itemPlacement = ip; + } + if ( event.getKey().equals(ChartPreferences.P_TEXTQUALITY) || event.getKey().equals(ChartPreferences.P_LINEQUALITY) ) { + String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY); + String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY); + LineQuality q1 = LineQuality.valueOf(s1); + LineQuality q2 = LineQuality.valueOf(s2); + if (trendNode!=null) trendNode.quality.textQuality = q1; + if (trendNode!=null) trendNode.quality.lineQuality = q2; + } + + } + }; + + /** + * The project which this editor is listening to for changes to + * {@link ChartKeys.ChartSourceKey keys}. + */ + IProject project; + + /** + * The model resource containing the input chart resource. + */ + Resource model; + + /** + * The text widget shown only if there is no IProject available at the time + * of editor part creation. + */ + Text errorText; + + /** + * A unique key for making DB requests chart editor specific without binding + * the requests to the editor object itself. + */ + UUID uniqueChartEditorId = UUID.randomUUID(); + Logger log; + Display display; + SWTChassis canvas; + CanvasContext cvsCtx; + TrendParticipant tp; + TrendNode trendNode; + StepListener stepListener; + MilestoneSpecListener milestoneListener; + MilestoneSpecQuery milestoneQuery; + + /** + * The ChartData instance used by this editor for sourcing data at any given + * moment. Project hint instances are copied into this instance. + */ + final ChartData chartData = new ChartData(null, null, null, null, null, null); + + /** + * The ChartSourceKey to match the model this editor was opened for. + * @see #model + * @see #init(IEditorSite, IEditorInput) + */ + ChartKeys.ChartSourceKey chartDataKey; + + + /** + * Context management utils + */ + protected IThreadWorkQueue swt; + protected ContextUtil contextUtil; + + class ExperimentStateListener implements IExperimentListener { + @Override + public void stateChanged(ExperimentState state) { + TrendSpec spec = trendNode.getTrendSpec(); + spec.experimentIsRunning = state == ExperimentState.RUNNING; + if (spec.experimentIsRunning && spec.viewProfile.trackExperimentTime) { + TrendParticipant t = tp; + if (t != null) + t.setDirty(); + } + } + } + + ExperimentStateListener experimentStateListener = new ExperimentStateListener(); + + class ChartDataListener extends HintListenerAdapter implements Runnable { + @Override + public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) { + if (key.equals(chartDataKey)) { + // @Thread any + if (!cvsCtx.isDisposed() && cvsCtx.isAlive()) { + cvsCtx.getThreadAccess().asyncExec(this); + } + } + } + @Override + public void run() { + // @Thread AWT + if (cvsCtx.isDisposed() || !cvsCtx.isAlive()) return; + ChartData data = Simantics.getProject().getHint(chartDataKey); + setInput( data, trendNode.getTrendSpec() ); + } + } + + ChartDataListener chartDataListener = new ChartDataListener(); + + class ValueTipBoxPositionListener extends HintListenerAdapter { + @Override + public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) { + if (key.equals(TrendParticipant.KEY_VALUE_TIP_BOX_RELATIVE_POS) && newValue != null) { + Session s = Simantics.getSession(); + ChartResource CHART = s.getService(ChartResource.class); + Point2D p = (Point2D) newValue; + double[] value = { p.getX(), p.getY() }; + s.asyncRequest(new SetProperty(getInputResource(), CHART.Chart_valueViewPosition, value, Bindings.DOUBLE_ARRAY)); + } + } + } + + ValueTipBoxPositionListener valueTipBoxPositionListener = new ValueTipBoxPositionListener(); + + // Link-Time + State linkTimeState; + IStateListener linkTimeStateListener = new IStateListener() { + @Override + public void handleStateChange(State state, Object oldValue) { + final ChartLinkData newData = (ChartLinkData) linkTimeState.getValue(); + trendNode.autoscaletime = newData == null || newData.sender == TimeSeriesEditor.this; + + if ( newData == null || newData.sender==TimeSeriesEditor.this ) return; + TrendNode tn = trendNode; + HorizRuler hr = tn!=null ? tn.horizRuler : null; + + ChartLinkData oldData = new ChartLinkData(); + getFromEnd(oldData); + + if ( hr != null && !ObjectUtils.objectEquals(tn.valueTipTime, newData.valueTipTime)) { + tn.valueTipTime = newData.valueTipTime; + tp.setDirty(); + } + + if ( hr != null && (oldData.from!=newData.from || oldData.sx!=newData.sx)) { + + cvsCtx.getThreadAccess().asyncExec( new Runnable() { + @Override + public void run() { + boolean b = trendNode.horizRuler.setFromScale(newData.from, newData.sx); + trendNode.horizRuler.autoscroll = false; + if (b) { + trendNode.layout(); + tp.setDirty(); + } + }}); + } + + } + }; + HorizRuler.TimeWindowListener horizRulerListener = new HorizRuler.TimeWindowListener() { + @Override + public void onNewWindow(double from, double end, double scalex) { + final ChartLinkData oldData = (ChartLinkData) linkTimeState.getValue(); + if (oldData != null) { + ChartLinkData data = new ChartLinkData(TimeSeriesEditor.this, from, end, scalex); + data.valueTipTime = trendNode.valueTipTime; + linkTimeState.setValue( data ); + } + } + }; + + class ChassisListener implements IChassisListener { + @Override + public void chassisClosed(ICanvasChassis sender) { + // Prevent deadlock while disposing which using syncExec would result in. + final ICanvasContext ctx = cvsCtx; + ThreadUtils.asyncExec(ctx.getThreadAccess(), new Runnable() { + @Override + public void run() { + if (ctx != null) { + AWTChassis awt = canvas.getAWTComponent(); + if (awt != null) + awt.setCanvasContext(null); + ctx.dispose(); + } + } + }); + canvas.removeChassisListener(ChassisListener.this); + } + } + + ActiveSelectionProvider selectionProvider = new ActiveSelectionProvider(); + MenuManager menuManager; + + public TimeSeriesEditor() { + log = Logger.getLogger( this.getClass().getName() ); + } + + boolean isTimeLinked() { + if (linkTimeState==null) return false; + Boolean isLinked = (Boolean) linkTimeState.getValue(); + return isLinked != null && isLinked; + } + + @Override + public void init(IEditorSite site, IEditorInput input) throws PartInitException { + super.init(site, input); + try { + this.model = Simantics.getSession().syncRequest( new Model( getInputResource() ) ); + this.chartDataKey = ChartKeys.chartSourceKey(model); + } catch (DatabaseException e) { + throw new PartInitException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Input " + getInputResource() + " is not part of a model.", e)); + } + } + + /** + * Invoke this only from the AWT thread. + * @param context + */ + protected void setCanvasContext(final SWTChassis chassis, final ICanvasContext context) { + // Cannot directly invoke SWTChassis.setCanvasContext only because it + // needs to be invoked in the SWT thread and AWTChassis.setCanvasContext in the + // AWT thread, but directly invoking SWTChassis.setCanvasContext would call both + // in the SWT thread which would cause synchronous scheduling of AWT + // runnables which is always a potential source of deadlocks. + chassis.getAWTComponent().setCanvasContext(context); + SWTUtils.asyncExec(chassis, new Runnable() { + @Override + public void run() { + if (!chassis.isDisposed()) + // For AWT, this is a no-operation. + chassis.setCanvasContext(context); + } + }); + } + + @Override + public void createPartControl(Composite parent) { + display = parent.getDisplay(); + swt = SWTThread.getThreadAccess(display); + + // Must have a project to attach to, otherwise the editor is useless. + project = Simantics.peekProject(); + if (project == null) { + errorText = new Text(parent, SWT.NONE); + errorText.setText("No project is open."); + errorText.setEditable(false); + return; + } + + // Create the canvas context here before finishing createPartControl + // to give anybody requiring access to this editor's ICanvasContext + // a chance to do their work. + // The context can be created in SWT thread without scheduling + // to the context thread and having potential deadlocks. + // The context is locked here and unlocked after it has been + // initialized in the AWT thread. + IThreadWorkQueue thread = AWTThread.getThreadAccess(); + cvsCtx = new CanvasContext(thread); + cvsCtx.setLocked(true); + + final IWorkbenchWindow win = getEditorSite().getWorkbenchWindow(); + final IWorkbenchPage page = getEditorSite().getPage(); + + canvas = new SWTChassis(parent, SWT.NONE); + canvas.populate(parameter -> { + if (!disposed) { + canvas.addChassisListener(new ChassisListener()); + initializeCanvas(canvas, cvsCtx, win, page); + } + }); + + // Link time + ICommandService service = (ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class); + Command command = service.getCommand( LinkTimeHandler.COMMAND_ID ); + linkTimeState = command.getState( LinkTimeHandler.STATE_ID ); + if ( linkTimeState != null ) linkTimeState.addListener( linkTimeStateListener ); + + addPopupMenu(); + + // Start tracking editor input validity. + activateValidation(); + + // Provide input as selection for property page. + selectionProvider.setSelection( new StructuredSelection(getInputResource()) ); + getSite().setSelectionProvider( selectionProvider ); + } + + protected void initializeCanvas(final SWTChassis chassis, CanvasContext cvsCtx, IWorkbenchWindow window, IWorkbenchPage page) { + // Initialize canvas context + TrendSpec nodata = new TrendSpec(); + nodata.init(); + cvsCtx = TrendInitializer.defaultInitializeCanvas(cvsCtx, null, null, null, nodata); + + tp = cvsCtx.getAtMostOneItemOfClass(TrendParticipant.class); + + + IContextService contextService = (IContextService) getSite().getService(IContextService.class); + contextUtil = new ContextUtil(contextService, swt); + + + cvsCtx.add( new SubscriptionDropParticipant( getInputResource() ) ); + cvsCtx.add( new KeyToCommand( ChartKeyBindings.DEFAULT_BINDINGS ) ); + cvsCtx.add( new ChartPasteHandler2(getInputResource().get()) ); + cvsCtx.add(contextUtil); + + // Context management + cvsCtx.add(new SGFocusParticipant(canvas, "org.simantics.charts.editor.context")); + + stepListener = new StepListener( tp ); + trendNode = tp.getTrend(); + trendNode.titleNode.remove(); + trendNode.titleNode = null; + + // Link time + trendNode.horizRuler.listener = horizRulerListener; + + final ChartLinkData linkTime = (ChartLinkData) linkTimeState.getValue(); + if (linkTime!=null) trendNode.horizRuler.setFromEnd(linkTime.from, linkTime.sx); + + // Handle mouse moved event after TrendParticipant. + // This handler forwards trend.mouseHoverTime to linkTimeState + cvsCtx.getEventHandlerStack().add( new IEventHandler() { + + @Override + public int getEventMask() { + return EventTypes.MouseMovedMask | EventTypes.MouseClickMask | EventTypes.CommandMask | EventTypes.KeyPressed; + } + + @Override + public boolean handleEvent(Event e) { + +// System.out.println("LinkEventHandler: "+e); + ChartLinkData oldData = (ChartLinkData) linkTimeState.getValue(); + if (oldData!=null) { + ChartLinkData newData = new ChartLinkData(); + getFromEnd(newData); + if (!newData.equals(oldData)) { +// System.out.println("Sending new link-data"); + linkTimeState.setValue( newData ); + } + } + return false; + }}, -1); + + canvas.getHintContext().setHint( SWTChassis.KEY_EDITORPART, this); + canvas.getHintContext().setHint( SWTChassis.KEY_WORKBENCHPAGE, page); + canvas.getHintContext().setHint( SWTChassis.KEY_WORKBENCHWINDOW, window); + + // Canvas context is initialized, unlock it now to allow rendering. + cvsCtx.setLocked(false); + + setCanvasContext(chassis, cvsCtx); + + cvsCtx.getEventHandlerStack().add(new IEventHandler() { + @Override + public boolean handleEvent(Event e) { + MouseButtonReleasedEvent event = (MouseButtonReleasedEvent) e; + if (event.button != MouseEvent.RIGHT_BUTTON) + return false; + + final Point p = new Point((int) event.screenPosition.getX(), (int) event.screenPosition.getY()); + SWTUtils.asyncExec(chassis, new Runnable() { + @Override + public void run() { + if (!canvas.isDisposed()) + showPopup(p); + } + }); + return true; + } + @Override + public int getEventMask() { + return EventTypes.MouseButtonReleasedMask; + } + }, 1000000); + + // Track data source and preinitialize chartData + project.addHintListener(chartDataListener); + chartData.readFrom( (ChartData) project.getHint( chartDataKey ) ); + + if (chartData.run != null) { + milestoneListener = new MilestoneSpecListener(); + milestoneQuery = new MilestoneSpecQuery( chartData.run ); + getSession().asyncRequest( milestoneQuery, milestoneListener ); + } + + // IMPORTANT: Only after preinitializing chartData, start tracking chart configuration + trackChartConfiguration(); + trackPreferences(); + + // Write changes to TrendSpec.viewProfile.valueViewPosition[XY] + // back to the graph database. + cvsCtx.getHintStack().addHintListener(valueTipBoxPositionListener); + } + + private void addPopupMenu() { + menuManager = new MenuManager("Time Series Editor", CONTEXT_MENU_ID); + menuManager.setRemoveAllWhenShown(true); + Menu menu = menuManager.createContextMenu(canvas); + canvas.setMenu(menu); + getEditorSite().registerContextMenu(menuManager.getId(), menuManager, selectionProvider); + + // Add support for some built-in actions in the context menu. + menuManager.addMenuListener(new IMenuListener() { + @Override + public void menuAboutToShow(IMenuManager manager) { + // Not initialized yet, prevent NPE. + TrendNode trendNode = TimeSeriesEditor.this.trendNode; + TrendParticipant tp = TimeSeriesEditor.this.tp; + if (trendNode == null || tp == null) + return; + + TrendSpec trendSpec = trendNode.getTrendSpec(); + ItemNode hoverItem = tp.hoveringItem; + if (hoverItem != null && hoverItem.item != null) { + Resource component = resolveReferencedComponent(getResourceInput(), hoverItem.item.variableId); + if (component != null) { + manager.add(new PerformDefaultAction("Show Referenced Component", canvas, component)); + } + + Resource chart = TimeSeriesEditor.this.getInputResource(); + if ( chart != null ) { + try { + Resource chartItem = getSession().sync( new FindChartItemForTrendItem(chart, hoverItem.item) ); + if (chartItem != null) { + manager.add(new HideItemsAction("Hide Item", true, Collections.singletonList(chartItem))); + manager.add(new Separator()); + manager.add(new PropertiesAction("Item Properties", canvas, chartItem)); + manager.add(new PropertiesAction("Chart Properties", canvas, chart)); + } + } catch (DatabaseException e) { + Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed to resolve context menu items.", e)); + } + } + } else { + boolean hairlineMovementAllowed = + !(trendSpec.experimentIsRunning && + trendSpec.viewProfile.trackExperimentTime); + + Resource chart = TimeSeriesEditor.this.getInputResource(); + manager.add(new Separator()); + manager.add(new MoveHairlineAction( + "Move Hairline Here", + chart, + hairlineMovementAllowed, + trendNode, + trendNode.mouseHoverTime + )); + manager.add(new MoveHairlineAction( + "Move Hairline To Current Time", + chart, + hairlineMovementAllowed, + trendNode, + trendNode.horizRuler.getItemEndTime(), + Boolean.FALSE + )); + manager.add(new TrackExperimentTimeAction( + "Hairline Tracks Current Time", + chart, + trendSpec.viewProfile.trackExperimentTime)); + manager.add(new Separator()); + manager.add(new SendCommandAction("Zoom to Fit", IMG_ZOOM_TO_FIT, cvsCtx, Commands.ZOOM_TO_FIT)); + manager.add(new SendCommandAction("Zoom to Fit Horizontally", IMG_ZOOM_TO_FIT_HORIZ, cvsCtx, Commands.ZOOM_TO_FIT_HORIZ)); + manager.add(new SendCommandAction("Zoom to Fit Vertically", IMG_ZOOM_TO_FIT_VERT, cvsCtx, Commands.ZOOM_TO_FIT_VERT)); + manager.add(new SendCommandAction("Autoscale Chart", IMG_AUTOSCALE, cvsCtx, Commands.AUTOSCALE)); + manager.add(new Separator()); + manager.add(new PropertiesAction("Chart Properties", canvas, chart)); + } + } + }); + } + + protected Resource resolveReferencedComponent(IResourceEditorInput resourceInput, final String variableId) { + try { + return getSession().sync(new UniqueRead() { + @Override + public Resource perform(ReadGraph graph) throws DatabaseException { + Variable configuration = Variables.getConfigurationContext(graph, getInputResource()); + RVI rvi = RVI.fromResourceFormat(graph, variableId); + rvi = new RVIBuilder(rvi).removeFromFirstRole(Role.PROPERTY).toRVI(); + if (rvi.isEmpty()) + return null; + Variable var = rvi.resolve(graph, configuration); + return var.getPossibleRepresents(graph); + } + }); + } catch (DatabaseException e) { + ErrorLogger.defaultLogError(e); + } + return null; + } + + private void showPopup(Point p) { + menuManager.getMenu().setLocation(p); + menuManager.getMenu().setVisible(true); + } + + private void trackChartConfiguration() { + getSession().asyncRequest(new TrendSpecQuery( uniqueChartEditorId, getInputResource() ), new TrendSpecListener()); + getSession().asyncRequest(new ActiveRunQuery( uniqueChartEditorId, getInputResource() ), new ActiveRunListener()); + } + + @Override + public void setFocus() { + if (errorText != null) + errorText.setFocus(); + else + canvas.setFocus(); + } + + @Override + public void dispose() { + if (disposed == true) return; + disposed = true; + + if (trendNode!=null && trendNode.horizRuler!=null) { + trendNode.horizRuler.listener = null; + } + + if ( linkTimeState != null ) linkTimeState.removeListener( linkTimeStateListener ); + + canvas.getHintContext().removeHint( SWTChassis.KEY_EDITORPART ); + canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHPAGE ); + canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHWINDOW ); + + if ( chartPreferenceNode!= null ) { + chartPreferenceNode.removePreferenceChangeListener( preferenceListener ); + } + + MilestoneSpecListener ml = milestoneListener; + if (ml!=null) ml.dispose(); + + if (project != null) { + project.removeHintListener(chartDataListener); + } + + if (chartData != null) { + if (chartData.datasource!=null) + chartData.datasource.removeListener( stepListener ); + if (chartData.experiment!=null) + chartData.experiment.removeListener( experimentStateListener ); + chartData.readFrom( null ); + } + + super.dispose(); + } + + /** + * @param data new data or null + * @param newSpec new spec or null + * @thread AWT + */ + @SuppressWarnings("unused") + public void setInput(ChartData data, TrendSpec newSpec) { + boolean doLayout = false; + + // Disregard input if it is not for this chart's containing model. + if (data != null && data.model != null && !data.model.equals(model)) + data = null; + + // Accommodate Datasource changes + Datasource: { + Datasource oldDatasource = chartData==null?null:chartData.datasource; + Datasource newDatasource = data==null?null:data.datasource; + //if ( !ObjectUtils.objectEquals(oldDatasource, newDatasource) ) + { + if (oldDatasource!=null) oldDatasource.removeListener( stepListener ); + if (newDatasource!=null) newDatasource.addListener( stepListener ); + } + } + + Experiment: { + IExperiment oldExperiment = chartData==null?null:chartData.experiment; + IExperiment newExperiment = data==null?null:data.experiment; + //if ( !ObjectUtils.objectEquals(oldExperiment, newExperiment) ) + { + if (oldExperiment!=null) oldExperiment.removeListener( experimentStateListener ); + if (newExperiment!=null) newExperiment.addListener( experimentStateListener ); + } + } + + // Accommodate Historian changes + Historian: { + HistoryManager oldHistorian = trendNode.historian==null?null:trendNode.historian; + HistoryManager newHistorian = data==null?null:data.history; + Collector newCollector = data==null?null:data.collector; + // if ( !ObjectUtils.objectEquals(oldHistorian, newHistorian) ) + { + if (newHistorian instanceof FileHistory) { + FileHistory fh = (FileHistory) newHistorian; + System.out.println("History = "+fh.getWorkarea()); + } + trendNode.setHistorian( newHistorian, newCollector ); + doLayout |= trendNode.autoscale(true, true) | !ObjectUtils.objectEquals(oldHistorian, newHistorian); + } + + // Accommodate TrendSpec changes + TrendSpec oldSpec = trendNode.getTrendSpec(); + if ( !newSpec.equals(oldSpec) ) { + trendNode.setTrendSpec( newSpec==null?TrendSpec.EMPTY:newSpec ); + doLayout = true; + } + + } + + Resource newExperimentResource = data==null ? null : data.run; + Resource oldExperimentResource = this.chartData == null ? null : this.chartData.run; + + // Track milestones + Milestones: { + if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) { + + // Dispose old listener & Query + if (milestoneListener!=null) { + milestoneListener.dispose(); + milestoneListener = null; + } + if (milestoneQuery!=null) { + milestoneQuery = null; + } + + trendNode.setMilestones( MilestoneSpec.EMPTY ); + + if (newExperimentResource != null) { + milestoneListener = new MilestoneSpecListener(); + milestoneQuery = new MilestoneSpecQuery( newExperimentResource ); + Simantics.getSession().asyncRequest( milestoneQuery, milestoneListener ); + } + } + + } + + if (doLayout) trendNode.layout(); + this.chartData.readFrom( data ); + tp.setDirty(); + + if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) { + resetViewAfterDataChange(); + } + + } + + class ActiveRunListener implements SyncListener { + @Override + public void exception(ReadGraph graph, Throwable throwable) { + ErrorLogger.defaultLogError(throwable); + ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage()); + } + @Override + public void execute(ReadGraph graph, final Resource run) throws DatabaseException { + if(run != null) { + SimulationResource SIMU = SimulationResource.getInstance(graph); + Variable var = Variables.getVariable(graph, run); + IExperiment exp = var.getPossiblePropertyValue(graph, SIMU.Run_iExperiment); + ITrendSupport ts = exp.getService(ITrendSupport.class); + if (ts != null) + ts.setChartData(graph); + } + } + @Override + public boolean isDisposed() { + return TimeSeriesEditor.this.disposed; + } + } + + class TrendSpecListener implements AsyncListener { + @Override + public void exception(AsyncReadGraph graph, Throwable throwable) { + + ErrorLogger.defaultLogError(throwable); + ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage()); + } + @Override + public void execute(AsyncReadGraph graph, final TrendSpec result) { + if (result == null) { + log.log(Level.INFO, "Chart configuration removed"); + } else { + log.log(Level.INFO, "Chart configuration updated: " + result); + } + + // Reload chart in AWT Thread + AWTThread.getThreadAccess().asyncExec(new Runnable() { + @Override + public void run() { + if (!disposed) + setInput( chartData, result ); + } + }); + } + @Override + public boolean isDisposed() { + return TimeSeriesEditor.this.disposed; + } + } + + class MilestoneSpecListener implements AsyncListener { + boolean disposed = false; + @Override + public void execute(AsyncReadGraph graph, final MilestoneSpec result) { + AWTThread.INSTANCE.asyncExec(new Runnable() { + public void run() { + trendNode.setMilestones(result); + }}); + } + + @Override + public void exception(AsyncReadGraph graph, Throwable throwable) { + + } + + @Override + public boolean isDisposed() { + return disposed; + } + + public void dispose() { + disposed = true; + } + + } + + private void trackPreferences() { + chartPreferenceNode = InstanceScope.INSTANCE.getNode( "org.simantics.charts" ); + chartPreferenceNode.addPreferenceChangeListener( preferenceListener ); + long redrawInterval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL); + long autoscaleInterval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL); + setInterval(redrawInterval, autoscaleInterval); + + String timeFormat = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT); + TimeFormat tf = TimeFormat.valueOf( timeFormat ); + if (tf!=null) setTimeFormat( tf ); + + Boolean drawSamples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES); + setDrawSamples(drawSamples); + + String valueFormat = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT); + ValueFormat vf = ValueFormat.valueOf( valueFormat ); + if (vf!=null) setValueFormat( vf ); + + String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT); + ItemPlacement ip = ItemPlacement.valueOf(s); + if (trendNode!=null) trendNode.itemPlacement = ip; + + String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY); + String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY); + LineQuality q1 = LineQuality.valueOf(s1); + LineQuality q2 = LineQuality.valueOf(s2); + if (trendNode!=null) trendNode.quality.textQuality = q1; + if (trendNode!=null) trendNode.quality.lineQuality = q2; + + } + + private void setInterval(long redrawInterval, long autoscaleInterval) { + redrawInterval = Math.max(1, redrawInterval); + long pulse = Math.min(50, redrawInterval); + pulse = Math.min(pulse, autoscaleInterval); + IHintContext h = canvas.getCanvasContext().getDefaultHintContext(); + h.setHint(TimeParticipant.KEY_TIME_PULSE_INTERVAL, pulse); + h.setHint(TrendParticipant.KEY_TREND_DRAW_INTERVAL, redrawInterval); + h.setHint(TrendParticipant.KEY_TREND_AUTOSCALE_INTERVAL, autoscaleInterval); + } + + private void setDrawSamples(boolean value) { + trendNode.drawSamples = value; + trendNode.layout(); + tp.setDirty(); + } + + private void setTimeFormat( TimeFormat tf ) { + if (trendNode.timeFormat == tf) return; + trendNode.timeFormat = tf; + trendNode.layout(); + tp.setDirty(); + } + + private void setValueFormat( ValueFormat vf ) { + if (trendNode.valueFormat == vf) return; + trendNode.valueFormat = vf; + trendNode.layout(); + tp.setDirty(); + } + + @SuppressWarnings("rawtypes") + @Override + public Object getAdapter(Class adapter) { + if (adapter == INode.class) { + ICanvasContext ctx = cvsCtx; + if (ctx != null) + return ctx.getSceneGraph(); + } + if (adapter == IPropertyPage.class) + return new StandardPropertyPage(getSite(), getPropertyPageContexts()); + if (adapter == ICanvasContext.class) + return cvsCtx; + return super.getAdapter(adapter); + } + + protected Set getPropertyPageContexts() { + try { + return BrowseContext.getBrowseContextClosure(Simantics.getSession(), Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT)); + } catch (DatabaseException e) { + ExceptionUtils.logAndShowError("Failed to load modeled browse contexts for property page, see exception for details.", e); + return Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT); + } + } + + /** + * Add from, end, (scale x) to argument array + * @param fromEnd array of 2 or 3 + */ + public void getFromEnd(ChartLinkData data) { + data.sender = this; + TrendNode tn = trendNode; + data.valueTipTime = tn.valueTipTime; + HorizRuler hr = tn!=null ? tn.horizRuler : null; + if ( hr != null ) { + data.from = hr.from; + data.end = hr.end; + double len = hr.end-hr.from; + double wid = tn.plot.getWidth(); + if ( wid==0.0 ) wid = 0.1; + data.sx = len/wid; + } + } + + @SuppressWarnings("unused") + private static boolean doubleEquals(double a, double b) { + if (Double.isNaN(a) && Double.isNaN(b)) return true; + return a==b; + } + + protected void resetViewAfterDataChange() { + + CanvasUtils.sendCommand(cvsCtx, Commands.CANCEL); + CanvasUtils.sendCommand(cvsCtx, Commands.AUTOSCALE); + + } + +}