1 /*******************************************************************************
2 * Copyright (c) 2007, 2011 Association for Decentralized Information Management in
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.charts.editor;
14 import java.awt.geom.Point2D;
15 import java.util.Collections;
17 import java.util.UUID;
18 import java.util.logging.Level;
19 import java.util.logging.Logger;
21 import org.eclipse.core.commands.Command;
22 import org.eclipse.core.commands.IStateListener;
23 import org.eclipse.core.commands.State;
24 import org.eclipse.core.runtime.IStatus;
25 import org.eclipse.core.runtime.Status;
26 import org.eclipse.core.runtime.preferences.IEclipsePreferences;
27 import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
28 import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
29 import org.eclipse.core.runtime.preferences.InstanceScope;
30 import org.eclipse.jface.action.IMenuListener;
31 import org.eclipse.jface.action.IMenuManager;
32 import org.eclipse.jface.action.MenuManager;
33 import org.eclipse.jface.action.Separator;
34 import org.eclipse.jface.resource.ImageDescriptor;
35 import org.eclipse.jface.viewers.StructuredSelection;
36 import org.eclipse.swt.SWT;
37 import org.eclipse.swt.graphics.Point;
38 import org.eclipse.swt.widgets.Composite;
39 import org.eclipse.swt.widgets.Display;
40 import org.eclipse.swt.widgets.Menu;
41 import org.eclipse.swt.widgets.Text;
42 import org.eclipse.ui.IEditorInput;
43 import org.eclipse.ui.IEditorSite;
44 import org.eclipse.ui.IWorkbenchPage;
45 import org.eclipse.ui.IWorkbenchWindow;
46 import org.eclipse.ui.PartInitException;
47 import org.eclipse.ui.PlatformUI;
48 import org.eclipse.ui.commands.ICommandService;
49 import org.eclipse.ui.contexts.IContextService;
50 import org.simantics.Simantics;
51 import org.simantics.browsing.ui.model.browsecontexts.BrowseContext;
52 import org.simantics.charts.Activator;
53 import org.simantics.charts.ITrendSupport;
54 import org.simantics.charts.ontology.ChartResource;
55 import org.simantics.charts.preference.ChartPreferences;
56 import org.simantics.charts.query.FindChartItemForTrendItem;
57 import org.simantics.charts.query.MilestoneSpecQuery;
58 import org.simantics.charts.query.SetProperty;
59 import org.simantics.charts.query.TrendSpecQuery;
60 import org.simantics.charts.ui.ChartLinkData;
61 import org.simantics.charts.ui.LinkTimeHandler;
62 import org.simantics.databoard.Bindings;
63 import org.simantics.databoard.util.ObjectUtils;
64 import org.simantics.db.AsyncReadGraph;
65 import org.simantics.db.ReadGraph;
66 import org.simantics.db.Resource;
67 import org.simantics.db.Session;
68 import org.simantics.db.common.request.ParametrizedRead;
69 import org.simantics.db.common.request.UniqueRead;
70 import org.simantics.db.exception.DatabaseException;
71 import org.simantics.db.layer0.request.Model;
72 import org.simantics.db.layer0.request.combinations.Combinators;
73 import org.simantics.db.layer0.variable.RVI;
74 import org.simantics.db.layer0.variable.RVIBuilder;
75 import org.simantics.db.layer0.variable.Variable;
76 import org.simantics.db.layer0.variable.Variables;
77 import org.simantics.db.layer0.variable.Variables.Role;
78 import org.simantics.db.procedure.AsyncListener;
79 import org.simantics.db.procedure.SyncListener;
80 import org.simantics.diagram.participant.ContextUtil;
81 import org.simantics.diagram.participant.SGFocusParticipant;
82 import org.simantics.g2d.canvas.ICanvasContext;
83 import org.simantics.g2d.canvas.impl.CanvasContext;
84 import org.simantics.g2d.chassis.AWTChassis;
85 import org.simantics.g2d.chassis.ICanvasChassis;
86 import org.simantics.g2d.chassis.IChassisListener;
87 import org.simantics.g2d.chassis.SWTChassis;
88 import org.simantics.g2d.participant.KeyToCommand;
89 import org.simantics.g2d.participant.TimeParticipant;
90 import org.simantics.g2d.utils.CanvasUtils;
91 import org.simantics.history.Collector;
92 import org.simantics.history.HistoryManager;
93 import org.simantics.history.impl.FileHistory;
94 import org.simantics.project.IProject;
95 import org.simantics.scenegraph.INode;
96 import org.simantics.scenegraph.g2d.events.Event;
97 import org.simantics.scenegraph.g2d.events.EventTypes;
98 import org.simantics.scenegraph.g2d.events.IEventHandler;
99 import org.simantics.scenegraph.g2d.events.MouseEvent;
100 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;
101 import org.simantics.scenegraph.g2d.events.command.Commands;
102 import org.simantics.selectionview.StandardPropertyPage;
103 import org.simantics.simulation.data.Datasource;
104 import org.simantics.simulation.experiment.ExperimentState;
105 import org.simantics.simulation.experiment.IExperiment;
106 import org.simantics.simulation.experiment.IExperimentListener;
107 import org.simantics.simulation.ontology.SimulationResource;
108 import org.simantics.trend.TrendInitializer;
109 import org.simantics.trend.TrendInitializer.StepListener;
110 import org.simantics.trend.configuration.ItemPlacement;
111 import org.simantics.trend.configuration.LineQuality;
112 import org.simantics.trend.configuration.TimeFormat;
113 import org.simantics.trend.configuration.TrendSpec;
114 import org.simantics.trend.impl.HorizRuler;
115 import org.simantics.trend.impl.ItemNode;
116 import org.simantics.trend.impl.MilestoneSpec;
117 import org.simantics.trend.impl.TrendNode;
118 import org.simantics.trend.impl.TrendParticipant;
119 import org.simantics.ui.workbench.IPropertyPage;
120 import org.simantics.ui.workbench.IResourceEditorInput;
121 import org.simantics.ui.workbench.ResourceEditorInput;
122 import org.simantics.ui.workbench.ResourceEditorPart;
123 import org.simantics.ui.workbench.action.PerformDefaultAction;
124 import org.simantics.ui.workbench.editor.input.InputValidationCombinators;
125 import org.simantics.utils.datastructures.hints.HintListenerAdapter;
126 import org.simantics.utils.datastructures.hints.IHintContext;
127 import org.simantics.utils.datastructures.hints.IHintContext.Key;
128 import org.simantics.utils.datastructures.hints.IHintObservable;
129 import org.simantics.utils.format.ValueFormat;
130 import org.simantics.utils.threads.AWTThread;
131 import org.simantics.utils.threads.IThreadWorkQueue;
132 import org.simantics.utils.threads.SWTThread;
133 import org.simantics.utils.threads.ThreadUtils;
134 import org.simantics.utils.ui.BundleUtils;
135 import org.simantics.utils.ui.ErrorLogger;
136 import org.simantics.utils.ui.ExceptionUtils;
137 import org.simantics.utils.ui.SWTUtils;
138 import org.simantics.utils.ui.dialogs.ShowMessage;
139 import org.simantics.utils.ui.jface.ActiveSelectionProvider;
142 * TimeSeriesEditor is an interactive part that draws a time series chart.
144 * The configuration model is {@link TrendSpec} which is read through
145 * {@link TrendSpecQuery}. In Simantics Environment the
146 * editor input is {@link ResourceEditorInput}.
148 * @author Toni Kalajainen <toni.kalajainen@vtt.fi>
149 * @author Tuukka Lehtonen
151 public class TimeSeriesEditor extends ResourceEditorPart {
153 ParametrizedRead<IResourceEditorInput, Boolean> INPUT_VALIDATOR =
155 InputValidationCombinators.hasURI(),
156 InputValidationCombinators.extractInputResource()
160 protected ParametrizedRead<IResourceEditorInput, Boolean> getInputValidator() {
161 return INPUT_VALIDATOR;
165 * The root property browse context of the time series editor. A transitive
166 * closure is calculated for this context.
168 private static String ROOT_PROPERTY_BROWSE_CONTEXT = ChartResource.URIs.ChartBrowseContext;
171 * ID of the this editor part extension.
173 public static final String ID = "org.simantics.charts.editor.timeseries";
175 private static final String CONTEXT_MENU_ID = "#timeSeriesChart";
177 private IEclipsePreferences chartPreferenceNode;
179 private final ImageDescriptor IMG_ZOOM_TO_FIT = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/horizAndVert16.png");
180 private final ImageDescriptor IMG_ZOOM_TO_FIT_HORIZ = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/horiz16.png");
181 private final ImageDescriptor IMG_ZOOM_TO_FIT_VERT = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/vert16.png");
182 private final ImageDescriptor IMG_AUTOSCALE = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/autoscale16.png");
184 IPreferenceChangeListener preferenceListener = new IPreferenceChangeListener() {
186 public void preferenceChange(PreferenceChangeEvent event) {
188 System.err.println("Warning: pref change to disposed TimeSeriesEditor");
192 if ( event.getKey().equals(ChartPreferences.P_REDRAW_INTERVAL ) ||
193 event.getKey().equals(ChartPreferences.P_AUTOSCALE_INTERVAL )) {
194 long redraw_interval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL);
195 long autoscale_interval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL);
196 setInterval( redraw_interval, autoscale_interval );
198 if ( event.getKey().equals(ChartPreferences.P_DRAW_SAMPLES )) {
199 boolean draw_samples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES);
200 setDrawSamples( draw_samples );
202 if ( event.getKey().equals(ChartPreferences.P_TIMEFORMAT ) ) {
203 String s = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT);
204 TimeFormat tf = TimeFormat.valueOf( s );
205 if (tf!=null) setTimeFormat( tf );
207 if ( event.getKey().equals(ChartPreferences.P_VALUEFORMAT ) ) {
208 String s = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT);
209 ValueFormat vf = ValueFormat.valueOf( s );
210 if (vf!=null) setValueFormat( vf );
212 if ( event.getKey().equals(ChartPreferences.P_ITEMPLACEMENT)) {
213 String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT);
214 ItemPlacement ip = ItemPlacement.valueOf(s);
215 if (trendNode!=null) trendNode.itemPlacement = ip;
217 if ( event.getKey().equals(ChartPreferences.P_TEXTQUALITY) || event.getKey().equals(ChartPreferences.P_LINEQUALITY) ) {
218 String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY);
219 String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY);
220 LineQuality q1 = LineQuality.valueOf(s1);
221 LineQuality q2 = LineQuality.valueOf(s2);
222 if (trendNode!=null) trendNode.quality.textQuality = q1;
223 if (trendNode!=null) trendNode.quality.lineQuality = q2;
230 * The project which this editor is listening to for changes to
231 * {@link ChartKeys.ChartSourceKey keys}.
236 * The model resource containing the input chart resource.
241 * The text widget shown only if there is no IProject available at the time
242 * of editor part creation.
247 * A unique key for making DB requests chart editor specific without binding
248 * the requests to the editor object itself.
250 UUID uniqueChartEditorId = UUID.randomUUID();
254 CanvasContext cvsCtx;
257 StepListener stepListener;
258 MilestoneSpecListener milestoneListener;
259 MilestoneSpecQuery milestoneQuery;
262 * The ChartData instance used by this editor for sourcing data at any given
263 * moment. Project hint instances are copied into this instance.
265 final ChartData chartData = new ChartData(null, null, null, null, null, null);
268 * The ChartSourceKey to match the model this editor was opened for.
270 * @see #init(IEditorSite, IEditorInput)
272 ChartKeys.ChartSourceKey chartDataKey;
276 * Context management utils
278 protected IThreadWorkQueue swt;
279 protected ContextUtil contextUtil;
281 class ExperimentStateListener implements IExperimentListener {
283 public void stateChanged(ExperimentState state) {
284 TrendSpec spec = trendNode.getTrendSpec();
285 spec.experimentIsRunning = state == ExperimentState.RUNNING;
286 if (spec.experimentIsRunning && spec.viewProfile.trackExperimentTime) {
287 TrendParticipant t = tp;
294 ExperimentStateListener experimentStateListener = new ExperimentStateListener();
296 class ChartDataListener extends HintListenerAdapter implements Runnable {
298 public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
299 if (key.equals(chartDataKey)) {
301 if (!cvsCtx.isDisposed() && cvsCtx.isAlive()) {
302 cvsCtx.getThreadAccess().asyncExec(this);
309 if (cvsCtx.isDisposed() || !cvsCtx.isAlive()) return;
310 ChartData data = Simantics.getProject().getHint(chartDataKey);
311 setInput( data, trendNode.getTrendSpec() );
315 ChartDataListener chartDataListener = new ChartDataListener();
317 class ValueTipBoxPositionListener extends HintListenerAdapter {
319 public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
320 if (key.equals(TrendParticipant.KEY_VALUE_TIP_BOX_RELATIVE_POS) && newValue != null) {
321 Session s = Simantics.getSession();
322 ChartResource CHART = s.getService(ChartResource.class);
323 Point2D p = (Point2D) newValue;
324 double[] value = { p.getX(), p.getY() };
325 s.asyncRequest(new SetProperty(getInputResource(), CHART.Chart_valueViewPosition, value, Bindings.DOUBLE_ARRAY));
330 ValueTipBoxPositionListener valueTipBoxPositionListener = new ValueTipBoxPositionListener();
334 IStateListener linkTimeStateListener = new IStateListener() {
336 public void handleStateChange(State state, Object oldValue) {
337 final ChartLinkData newData = (ChartLinkData) linkTimeState.getValue();
338 trendNode.autoscaletime = newData == null || newData.sender == TimeSeriesEditor.this;
340 if ( newData == null || newData.sender==TimeSeriesEditor.this ) return;
341 TrendNode tn = trendNode;
342 HorizRuler hr = tn!=null ? tn.horizRuler : null;
344 ChartLinkData oldData = new ChartLinkData();
347 if ( hr != null && !ObjectUtils.objectEquals(tn.valueTipTime, newData.valueTipTime)) {
348 tn.valueTipTime = newData.valueTipTime;
352 if ( hr != null && (oldData.from!=newData.from || oldData.sx!=newData.sx)) {
354 cvsCtx.getThreadAccess().asyncExec( new Runnable() {
357 boolean b = trendNode.horizRuler.setFromScale(newData.from, newData.sx);
358 trendNode.horizRuler.autoscroll = false;
368 HorizRuler.TimeWindowListener horizRulerListener = new HorizRuler.TimeWindowListener() {
370 public void onNewWindow(double from, double end, double scalex) {
371 final ChartLinkData oldData = (ChartLinkData) linkTimeState.getValue();
372 if (oldData != null) {
373 ChartLinkData data = new ChartLinkData(TimeSeriesEditor.this, from, end, scalex);
374 data.valueTipTime = trendNode.valueTipTime;
375 linkTimeState.setValue( data );
380 class ChassisListener implements IChassisListener {
382 public void chassisClosed(ICanvasChassis sender) {
383 // Prevent deadlock while disposing which using syncExec would result in.
384 final ICanvasContext ctx = cvsCtx;
385 ThreadUtils.asyncExec(ctx.getThreadAccess(), new Runnable() {
389 AWTChassis awt = canvas.getAWTComponent();
391 awt.setCanvasContext(null);
396 canvas.removeChassisListener(ChassisListener.this);
400 ActiveSelectionProvider selectionProvider = new ActiveSelectionProvider();
401 MenuManager menuManager;
403 public TimeSeriesEditor() {
404 log = Logger.getLogger( this.getClass().getName() );
407 boolean isTimeLinked() {
408 if (linkTimeState==null) return false;
409 Boolean isLinked = (Boolean) linkTimeState.getValue();
410 return isLinked != null && isLinked;
414 public void init(IEditorSite site, IEditorInput input) throws PartInitException {
415 super.init(site, input);
417 this.model = Simantics.getSession().syncRequest( new Model( getInputResource() ) );
418 this.chartDataKey = ChartKeys.chartSourceKey(model);
419 } catch (DatabaseException e) {
420 throw new PartInitException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Input " + getInputResource() + " is not part of a model.", e));
425 * Invoke this only from the AWT thread.
428 protected void setCanvasContext(final SWTChassis chassis, final ICanvasContext context) {
429 // Cannot directly invoke SWTChassis.setCanvasContext only because it
430 // needs to be invoked in the SWT thread and AWTChassis.setCanvasContext in the
431 // AWT thread, but directly invoking SWTChassis.setCanvasContext would call both
432 // in the SWT thread which would cause synchronous scheduling of AWT
433 // runnables which is always a potential source of deadlocks.
434 chassis.getAWTComponent().setCanvasContext(context);
435 SWTUtils.asyncExec(chassis, new Runnable() {
438 if (!chassis.isDisposed())
439 // For AWT, this is a no-operation.
440 chassis.setCanvasContext(context);
446 public void createPartControl(Composite parent) {
447 display = parent.getDisplay();
448 swt = SWTThread.getThreadAccess(display);
450 // Must have a project to attach to, otherwise the editor is useless.
451 project = Simantics.peekProject();
452 if (project == null) {
453 errorText = new Text(parent, SWT.NONE);
454 errorText.setText("No project is open.");
455 errorText.setEditable(false);
459 // Create the canvas context here before finishing createPartControl
460 // to give anybody requiring access to this editor's ICanvasContext
461 // a chance to do their work.
462 // The context can be created in SWT thread without scheduling
463 // to the context thread and having potential deadlocks.
464 // The context is locked here and unlocked after it has been
465 // initialized in the AWT thread.
466 IThreadWorkQueue thread = AWTThread.getThreadAccess();
467 cvsCtx = new CanvasContext(thread);
468 cvsCtx.setLocked(true);
470 final IWorkbenchWindow win = getEditorSite().getWorkbenchWindow();
471 final IWorkbenchPage page = getEditorSite().getPage();
473 canvas = new SWTChassis(parent, SWT.NONE);
474 canvas.populate(parameter -> {
476 canvas.addChassisListener(new ChassisListener());
477 initializeCanvas(canvas, cvsCtx, win, page);
482 ICommandService service = (ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class);
483 Command command = service.getCommand( LinkTimeHandler.COMMAND_ID );
484 linkTimeState = command.getState( LinkTimeHandler.STATE_ID );
485 if ( linkTimeState != null ) linkTimeState.addListener( linkTimeStateListener );
489 // Start tracking editor input validity.
490 activateValidation();
492 // Provide input as selection for property page.
493 selectionProvider.setSelection( new StructuredSelection(getInputResource()) );
494 getSite().setSelectionProvider( selectionProvider );
497 protected void initializeCanvas(final SWTChassis chassis, CanvasContext cvsCtx, IWorkbenchWindow window, IWorkbenchPage page) {
498 // Initialize canvas context
499 TrendSpec nodata = new TrendSpec();
501 cvsCtx = TrendInitializer.defaultInitializeCanvas(cvsCtx, null, null, null, nodata);
503 tp = cvsCtx.getAtMostOneItemOfClass(TrendParticipant.class);
506 IContextService contextService = (IContextService) getSite().getService(IContextService.class);
507 contextUtil = new ContextUtil(contextService, swt);
510 cvsCtx.add( new SubscriptionDropParticipant( getInputResource() ) );
511 cvsCtx.add( new KeyToCommand( ChartKeyBindings.DEFAULT_BINDINGS ) );
512 cvsCtx.add( new ChartPasteHandler2(getInputResource().get()) );
513 cvsCtx.add(contextUtil);
515 // Context management
516 cvsCtx.add(new SGFocusParticipant(canvas, "org.simantics.charts.editor.context"));
518 stepListener = new StepListener( tp );
519 trendNode = tp.getTrend();
520 trendNode.titleNode.remove();
521 trendNode.titleNode = null;
524 trendNode.horizRuler.listener = horizRulerListener;
526 final ChartLinkData linkTime = (ChartLinkData) linkTimeState.getValue();
527 if (linkTime!=null) trendNode.horizRuler.setFromEnd(linkTime.from, linkTime.sx);
529 // Handle mouse moved event after TrendParticipant.
530 // This handler forwards trend.mouseHoverTime to linkTimeState
531 cvsCtx.getEventHandlerStack().add( new IEventHandler() {
534 public int getEventMask() {
535 return EventTypes.MouseMovedMask | EventTypes.MouseClickMask | EventTypes.CommandMask | EventTypes.KeyPressed;
539 public boolean handleEvent(Event e) {
541 // System.out.println("LinkEventHandler: "+e);
542 ChartLinkData oldData = (ChartLinkData) linkTimeState.getValue();
544 ChartLinkData newData = new ChartLinkData();
546 if (!newData.equals(oldData)) {
547 // System.out.println("Sending new link-data");
548 linkTimeState.setValue( newData );
554 canvas.getHintContext().setHint( SWTChassis.KEY_EDITORPART, this);
555 canvas.getHintContext().setHint( SWTChassis.KEY_WORKBENCHPAGE, page);
556 canvas.getHintContext().setHint( SWTChassis.KEY_WORKBENCHWINDOW, window);
558 // Canvas context is initialized, unlock it now to allow rendering.
559 cvsCtx.setLocked(false);
561 setCanvasContext(chassis, cvsCtx);
563 cvsCtx.getEventHandlerStack().add(new IEventHandler() {
565 public boolean handleEvent(Event e) {
566 MouseButtonReleasedEvent event = (MouseButtonReleasedEvent) e;
567 if (event.button != MouseEvent.RIGHT_BUTTON)
570 final Point p = new Point((int) event.screenPosition.getX(), (int) event.screenPosition.getY());
571 SWTUtils.asyncExec(chassis, new Runnable() {
574 if (!canvas.isDisposed())
581 public int getEventMask() {
582 return EventTypes.MouseButtonReleasedMask;
586 // Track data source and preinitialize chartData
587 project.addHintListener(chartDataListener);
588 chartData.readFrom( (ChartData) project.getHint( chartDataKey ) );
589 chartData.reference();
591 if (chartData.run != null) {
592 milestoneListener = new MilestoneSpecListener();
593 milestoneQuery = new MilestoneSpecQuery( chartData.run );
594 getSession().asyncRequest( milestoneQuery, milestoneListener );
597 // IMPORTANT: Only after preinitializing chartData, start tracking chart configuration
598 trackChartConfiguration();
601 // Write changes to TrendSpec.viewProfile.valueViewPosition[XY]
602 // back to the graph database.
603 cvsCtx.getHintStack().addHintListener(valueTipBoxPositionListener);
606 private void addPopupMenu() {
607 menuManager = new MenuManager("Time Series Editor", CONTEXT_MENU_ID);
608 menuManager.setRemoveAllWhenShown(true);
609 Menu menu = menuManager.createContextMenu(canvas);
610 canvas.setMenu(menu);
611 getEditorSite().registerContextMenu(menuManager.getId(), menuManager, selectionProvider);
613 // Add support for some built-in actions in the context menu.
614 menuManager.addMenuListener(new IMenuListener() {
616 public void menuAboutToShow(IMenuManager manager) {
617 // Not initialized yet, prevent NPE.
618 TrendNode trendNode = TimeSeriesEditor.this.trendNode;
619 TrendParticipant tp = TimeSeriesEditor.this.tp;
620 if (trendNode == null || tp == null)
623 TrendSpec trendSpec = trendNode.getTrendSpec();
624 ItemNode hoverItem = tp.hoveringItem;
625 if (hoverItem != null && hoverItem.item != null) {
626 Resource component = resolveReferencedComponent(getResourceInput(), hoverItem.item.variableId);
627 if (component != null) {
628 manager.add(new PerformDefaultAction("Show Referenced Component", canvas, component));
631 Resource chart = TimeSeriesEditor.this.getInputResource();
632 if ( chart != null ) {
634 Resource chartItem = getSession().sync( new FindChartItemForTrendItem(chart, hoverItem.item) );
635 if (chartItem != null) {
636 manager.add(new HideItemsAction("Hide Item", true, Collections.singletonList(chartItem)));
637 manager.add(new Separator());
638 manager.add(new PropertiesAction("Item Properties", canvas, chartItem));
639 manager.add(new PropertiesAction("Chart Properties", canvas, chart));
641 } catch (DatabaseException e) {
642 Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed to resolve context menu items.", e));
646 boolean hairlineMovementAllowed =
647 !(trendSpec.experimentIsRunning &&
648 trendSpec.viewProfile.trackExperimentTime);
650 Resource chart = TimeSeriesEditor.this.getInputResource();
651 manager.add(new Separator());
652 manager.add(new MoveHairlineAction(
653 "Move Hairline Here",
655 hairlineMovementAllowed,
657 trendNode.mouseHoverTime
659 manager.add(new MoveHairlineAction(
660 "Move Hairline To Current Time",
662 hairlineMovementAllowed,
664 trendNode.horizRuler.getItemEndTime(),
667 manager.add(new TrackExperimentTimeAction(
668 "Hairline Tracks Current Time",
670 trendSpec.viewProfile.trackExperimentTime));
671 manager.add(new Separator());
672 manager.add(new SendCommandAction("Zoom to Fit", IMG_ZOOM_TO_FIT, cvsCtx, Commands.ZOOM_TO_FIT));
673 manager.add(new SendCommandAction("Zoom to Fit Horizontally", IMG_ZOOM_TO_FIT_HORIZ, cvsCtx, Commands.ZOOM_TO_FIT_HORIZ));
674 manager.add(new SendCommandAction("Zoom to Fit Vertically", IMG_ZOOM_TO_FIT_VERT, cvsCtx, Commands.ZOOM_TO_FIT_VERT));
675 manager.add(new SendCommandAction("Autoscale Chart", IMG_AUTOSCALE, cvsCtx, Commands.AUTOSCALE));
676 manager.add(new Separator());
677 manager.add(new PropertiesAction("Chart Properties", canvas, chart));
683 protected Resource resolveReferencedComponent(IResourceEditorInput resourceInput, final String variableId) {
685 return getSession().sync(new UniqueRead<Resource>() {
687 public Resource perform(ReadGraph graph) throws DatabaseException {
688 Variable configuration = Variables.getConfigurationContext(graph, getInputResource());
689 RVI rvi = RVI.fromResourceFormat(graph, variableId);
690 rvi = new RVIBuilder(rvi).removeFromFirstRole(Role.PROPERTY).toRVI();
693 Variable var = rvi.resolve(graph, configuration);
694 return var.getPossibleRepresents(graph);
697 } catch (DatabaseException e) {
698 ErrorLogger.defaultLogError(e);
703 private void showPopup(Point p) {
704 menuManager.getMenu().setLocation(p);
705 menuManager.getMenu().setVisible(true);
708 private void trackChartConfiguration() {
709 getSession().asyncRequest(new TrendSpecQuery( uniqueChartEditorId, getInputResource() ), new TrendSpecListener());
710 getSession().asyncRequest(new ActiveRunQuery( uniqueChartEditorId, getInputResource() ), new ActiveRunListener());
714 public void setFocus() {
715 if (errorText != null)
716 errorText.setFocus();
722 public void dispose() {
723 if (disposed == true) return;
726 if (trendNode!=null && trendNode.horizRuler!=null) {
727 trendNode.horizRuler.listener = null;
730 if ( linkTimeState != null ) linkTimeState.removeListener( linkTimeStateListener );
732 canvas.getHintContext().removeHint( SWTChassis.KEY_EDITORPART );
733 canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHPAGE );
734 canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHWINDOW );
736 if ( chartPreferenceNode!= null ) {
737 chartPreferenceNode.removePreferenceChangeListener( preferenceListener );
740 MilestoneSpecListener ml = milestoneListener;
741 if (ml!=null) ml.dispose();
743 if (project != null) {
744 project.removeHintListener(chartDataListener);
747 if (chartData != null) {
748 if (chartData.datasource!=null)
749 chartData.datasource.removeListener( stepListener );
750 if (chartData.experiment!=null)
751 chartData.experiment.removeListener( experimentStateListener );
752 chartData.dereference();
753 chartData.readFrom( null );
760 * @param data new data or null
761 * @param newSpec new spec or null
764 @SuppressWarnings("unused")
765 public void setInput(ChartData data, TrendSpec newSpec) {
766 boolean doLayout = false;
768 // Disregard input if it is not for this chart's containing model.
769 if (data != null && data.model != null && !data.model.equals(model))
772 // Accommodate Datasource changes
774 Datasource oldDatasource = chartData==null?null:chartData.datasource;
775 Datasource newDatasource = data==null?null:data.datasource;
776 //if ( !ObjectUtils.objectEquals(oldDatasource, newDatasource) )
778 if (oldDatasource!=null) oldDatasource.removeListener( stepListener );
779 if (newDatasource!=null) newDatasource.addListener( stepListener );
784 IExperiment oldExperiment = chartData==null?null:chartData.experiment;
785 IExperiment newExperiment = data==null?null:data.experiment;
786 //if ( !ObjectUtils.objectEquals(oldExperiment, newExperiment) )
788 if (oldExperiment!=null) oldExperiment.removeListener( experimentStateListener );
789 if (newExperiment!=null) newExperiment.addListener( experimentStateListener );
793 // Accommodate Historian changes
795 HistoryManager oldHistorian = trendNode.historian==null?null:trendNode.historian;
796 HistoryManager newHistorian = data==null?null:data.history;
797 Collector newCollector = data==null?null:data.collector;
798 // if ( !ObjectUtils.objectEquals(oldHistorian, newHistorian) )
800 if (newHistorian instanceof FileHistory) {
801 FileHistory fh = (FileHistory) newHistorian;
802 System.out.println("History = "+fh.getWorkarea());
804 trendNode.setHistorian( newHistorian, newCollector );
805 doLayout |= trendNode.autoscale(true, true) | !ObjectUtils.objectEquals(oldHistorian, newHistorian);
808 // Accommodate TrendSpec changes
809 TrendSpec oldSpec = trendNode.getTrendSpec();
810 if ( !newSpec.equals(oldSpec) ) {
811 trendNode.setTrendSpec( newSpec==null?TrendSpec.EMPTY:newSpec );
817 Resource newExperimentResource = data==null ? null : data.run;
818 Resource oldExperimentResource = this.chartData == null ? null : this.chartData.run;
822 if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {
824 // Dispose old listener & Query
825 if (milestoneListener!=null) {
826 milestoneListener.dispose();
827 milestoneListener = null;
829 if (milestoneQuery!=null) {
830 milestoneQuery = null;
833 trendNode.setMilestones( MilestoneSpec.EMPTY );
835 if (newExperimentResource != null) {
836 milestoneListener = new MilestoneSpecListener();
837 milestoneQuery = new MilestoneSpecQuery( newExperimentResource );
838 Simantics.getSession().asyncRequest( milestoneQuery, milestoneListener );
844 if (doLayout) trendNode.layout();
845 this.chartData.dereference();
846 this.chartData.readFrom( data );
847 this.chartData.reference();
850 if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {
851 resetViewAfterDataChange();
856 class ActiveRunListener implements SyncListener<Resource> {
858 public void exception(ReadGraph graph, Throwable throwable) {
859 ErrorLogger.defaultLogError(throwable);
860 ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());
863 public void execute(ReadGraph graph, final Resource run) throws DatabaseException {
865 SimulationResource SIMU = SimulationResource.getInstance(graph);
866 Variable var = Variables.getPossibleVariable(graph, run);
867 IExperiment exp = var != null ? var.getPossiblePropertyValue(graph, SIMU.Run_iExperiment) : null;
868 ITrendSupport ts = exp != null ? exp.getService(ITrendSupport.class) : null;
870 ts.setChartData(graph);
874 public boolean isDisposed() {
875 return TimeSeriesEditor.this.disposed;
879 class TrendSpecListener implements AsyncListener<TrendSpec> {
881 public void exception(AsyncReadGraph graph, Throwable throwable) {
883 ErrorLogger.defaultLogError(throwable);
884 ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());
887 public void execute(AsyncReadGraph graph, final TrendSpec result) {
888 if (result == null) {
889 log.log(Level.INFO, "Chart configuration removed");
891 log.log(Level.INFO, "Chart configuration updated: " + result);
894 // Reload chart in AWT Thread
895 AWTThread.getThreadAccess().asyncExec(new Runnable() {
899 setInput( chartData, result );
904 public boolean isDisposed() {
905 return TimeSeriesEditor.this.disposed;
909 class MilestoneSpecListener implements AsyncListener<MilestoneSpec> {
910 boolean disposed = false;
912 public void execute(AsyncReadGraph graph, final MilestoneSpec result) {
913 AWTThread.INSTANCE.asyncExec(new Runnable() {
915 trendNode.setMilestones(result);
920 public void exception(AsyncReadGraph graph, Throwable throwable) {
925 public boolean isDisposed() {
929 public void dispose() {
935 private void trackPreferences() {
936 chartPreferenceNode = InstanceScope.INSTANCE.getNode( "org.simantics.charts" );
937 chartPreferenceNode.addPreferenceChangeListener( preferenceListener );
938 long redrawInterval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL);
939 long autoscaleInterval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL);
940 setInterval(redrawInterval, autoscaleInterval);
942 String timeFormat = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT);
943 TimeFormat tf = TimeFormat.valueOf( timeFormat );
944 if (tf!=null) setTimeFormat( tf );
946 Boolean drawSamples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES);
947 setDrawSamples(drawSamples);
949 String valueFormat = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT);
950 ValueFormat vf = ValueFormat.valueOf( valueFormat );
951 if (vf!=null) setValueFormat( vf );
953 String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT);
954 ItemPlacement ip = ItemPlacement.valueOf(s);
955 if (trendNode!=null) trendNode.itemPlacement = ip;
957 String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY);
958 String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY);
959 LineQuality q1 = LineQuality.valueOf(s1);
960 LineQuality q2 = LineQuality.valueOf(s2);
961 if (trendNode!=null) trendNode.quality.textQuality = q1;
962 if (trendNode!=null) trendNode.quality.lineQuality = q2;
966 private void setInterval(long redrawInterval, long autoscaleInterval) {
967 redrawInterval = Math.max(1, redrawInterval);
968 long pulse = Math.min(50, redrawInterval);
969 pulse = Math.min(pulse, autoscaleInterval);
970 IHintContext h = canvas.getCanvasContext().getDefaultHintContext();
971 h.setHint(TimeParticipant.KEY_TIME_PULSE_INTERVAL, pulse);
972 h.setHint(TrendParticipant.KEY_TREND_DRAW_INTERVAL, redrawInterval);
973 h.setHint(TrendParticipant.KEY_TREND_AUTOSCALE_INTERVAL, autoscaleInterval);
976 private void setDrawSamples(boolean value) {
977 trendNode.drawSamples = value;
982 private void setTimeFormat( TimeFormat tf ) {
983 if (trendNode.timeFormat == tf) return;
984 trendNode.timeFormat = tf;
989 private void setValueFormat( ValueFormat vf ) {
990 if (trendNode.valueFormat == vf) return;
991 trendNode.valueFormat = vf;
996 @SuppressWarnings("unchecked")
998 public <T> T getAdapter(Class<T> adapter) {
999 if (adapter == INode.class) {
1000 ICanvasContext ctx = cvsCtx;
1002 return (T) ctx.getSceneGraph();
1004 if (adapter == IPropertyPage.class)
1005 return (T) new StandardPropertyPage(getSite(), getPropertyPageContexts());
1006 if (adapter == ICanvasContext.class)
1008 return super.getAdapter(adapter);
1011 protected Set<String> getPropertyPageContexts() {
1013 return BrowseContext.getBrowseContextClosure(Simantics.getSession(), Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT));
1014 } catch (DatabaseException e) {
1015 ExceptionUtils.logAndShowError("Failed to load modeled browse contexts for property page, see exception for details.", e);
1016 return Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT);
1021 * Add from, end, (scale x) to argument array
1022 * @param fromEnd array of 2 or 3
1024 public void getFromEnd(ChartLinkData data) {
1026 TrendNode tn = trendNode;
1027 data.valueTipTime = tn.valueTipTime;
1028 HorizRuler hr = tn!=null ? tn.horizRuler : null;
1030 data.from = hr.from;
1032 double len = hr.end-hr.from;
1033 double wid = tn.plot.getWidth();
1034 if ( wid==0.0 ) wid = 0.1;
1039 @SuppressWarnings("unused")
1040 private static boolean doubleEquals(double a, double b) {
1041 if (Double.isNaN(a) && Double.isNaN(b)) return true;
1045 protected void resetViewAfterDataChange() {
1047 CanvasUtils.sendCommand(cvsCtx, Commands.CANCEL);
1048 CanvasUtils.sendCommand(cvsCtx, Commands.AUTOSCALE);