/******************************************************************************* * 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); } }