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=d83a37ad4f665241aed27d1b3618b1a549216714;hp=4377eddc6076782c1997ea766618506e1f23c1e9;hb=0ae2b770234dfc3cbb18bd38f324125cf0faca07;hpb=24e2b34260f219f0d1644ca7a138894980e25b14 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 index 4377eddc6..d83a37ad4 100644 --- a/bundles/org.simantics.charts/src/org/simantics/charts/editor/TimeSeriesEditor.java +++ b/bundles/org.simantics.charts/src/org/simantics/charts/editor/TimeSeriesEditor.java @@ -1,1048 +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); - - } - -} +/******************************************************************************* + * 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); + + } + +}