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.SWTDPIUtil;
138 import org.simantics.utils.ui.SWTUtils;
139 import org.simantics.utils.ui.dialogs.ShowMessage;
140 import org.simantics.utils.ui.jface.ActiveSelectionProvider;
143 * TimeSeriesEditor is an interactive part that draws a time series chart.
145 * The configuration model is {@link TrendSpec} which is read through
146 * {@link TrendSpecQuery}. In Simantics Environment the
147 * editor input is {@link ResourceEditorInput}.
149 * @author Toni Kalajainen <toni.kalajainen@vtt.fi>
150 * @author Tuukka Lehtonen
152 public class TimeSeriesEditor extends ResourceEditorPart {
154 ParametrizedRead<IResourceEditorInput, Boolean> INPUT_VALIDATOR =
156 InputValidationCombinators.hasURI(),
157 InputValidationCombinators.extractInputResource()
161 protected ParametrizedRead<IResourceEditorInput, Boolean> getInputValidator() {
162 return INPUT_VALIDATOR;
166 * The root property browse context of the time series editor. A transitive
167 * closure is calculated for this context.
169 private static String ROOT_PROPERTY_BROWSE_CONTEXT = ChartResource.URIs.ChartBrowseContext;
172 * ID of the this editor part extension.
174 public static final String ID = "org.simantics.charts.editor.timeseries";
176 private static final String CONTEXT_MENU_ID = "#timeSeriesChart";
178 private IEclipsePreferences chartPreferenceNode;
180 private final ImageDescriptor IMG_ZOOM_TO_FIT = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/horizAndVert16.png");
181 private final ImageDescriptor IMG_ZOOM_TO_FIT_HORIZ = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/horiz16.png");
182 private final ImageDescriptor IMG_ZOOM_TO_FIT_VERT = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/vert16.png");
183 private final ImageDescriptor IMG_AUTOSCALE = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/autoscale16.png");
185 IPreferenceChangeListener preferenceListener = new IPreferenceChangeListener() {
187 public void preferenceChange(PreferenceChangeEvent event) {
189 System.err.println("Warning: pref change to disposed TimeSeriesEditor");
193 if ( event.getKey().equals(ChartPreferences.P_REDRAW_INTERVAL ) ||
194 event.getKey().equals(ChartPreferences.P_AUTOSCALE_INTERVAL )) {
195 long redraw_interval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL);
196 long autoscale_interval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL);
197 setInterval( redraw_interval, autoscale_interval );
199 if ( event.getKey().equals(ChartPreferences.P_DRAW_SAMPLES )) {
200 boolean draw_samples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES);
201 setDrawSamples( draw_samples );
203 if ( event.getKey().equals(ChartPreferences.P_TIMEFORMAT ) ) {
204 String s = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT);
205 TimeFormat tf = TimeFormat.valueOf( s );
206 if (tf!=null) setTimeFormat( tf );
208 if ( event.getKey().equals(ChartPreferences.P_VALUEFORMAT ) ) {
209 String s = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT);
210 ValueFormat vf = ValueFormat.valueOf( s );
211 if (vf!=null) setValueFormat( vf );
213 if ( event.getKey().equals(ChartPreferences.P_ITEMPLACEMENT)) {
214 String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT);
215 ItemPlacement ip = ItemPlacement.valueOf(s);
216 if (trendNode!=null) trendNode.itemPlacement = ip;
218 if ( event.getKey().equals(ChartPreferences.P_TEXTQUALITY) || event.getKey().equals(ChartPreferences.P_LINEQUALITY) ) {
219 String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY);
220 String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY);
221 LineQuality q1 = LineQuality.valueOf(s1);
222 LineQuality q2 = LineQuality.valueOf(s2);
223 if (trendNode!=null) trendNode.quality.textQuality = q1;
224 if (trendNode!=null) trendNode.quality.lineQuality = q2;
231 * The project which this editor is listening to for changes to
232 * {@link ChartKeys.ChartSourceKey keys}.
237 * The model resource containing the input chart resource.
242 * The text widget shown only if there is no IProject available at the time
243 * of editor part creation.
248 * A unique key for making DB requests chart editor specific without binding
249 * the requests to the editor object itself.
251 UUID uniqueChartEditorId = UUID.randomUUID();
255 CanvasContext cvsCtx;
258 StepListener stepListener;
259 MilestoneSpecListener milestoneListener;
260 MilestoneSpecQuery milestoneQuery;
263 * The ChartData instance used by this editor for sourcing data at any given
264 * moment. Project hint instances are copied into this instance.
266 final ChartData chartData = new ChartData(null, null, null, null, null, null);
269 * The ChartSourceKey to match the model this editor was opened for.
271 * @see #init(IEditorSite, IEditorInput)
273 ChartKeys.ChartSourceKey chartDataKey;
277 * Context management utils
279 protected IThreadWorkQueue swt;
280 protected ContextUtil contextUtil;
282 class ExperimentStateListener implements IExperimentListener {
284 public void stateChanged(ExperimentState state) {
285 TrendSpec spec = trendNode.getTrendSpec();
286 spec.experimentIsRunning = state == ExperimentState.RUNNING;
287 if (spec.experimentIsRunning && spec.viewProfile.trackExperimentTime) {
288 TrendParticipant t = tp;
295 ExperimentStateListener experimentStateListener = new ExperimentStateListener();
297 class ChartDataListener extends HintListenerAdapter implements Runnable {
299 public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
300 if (key.equals(chartDataKey)) {
302 if (!cvsCtx.isDisposed() && cvsCtx.isAlive()) {
303 cvsCtx.getThreadAccess().asyncExec(this);
310 if (cvsCtx.isDisposed() || !cvsCtx.isAlive()) return;
311 ChartData data = Simantics.getProject().getHint(chartDataKey);
312 setInput( data, trendNode.getTrendSpec() );
316 ChartDataListener chartDataListener = new ChartDataListener();
318 class ValueTipBoxPositionListener extends HintListenerAdapter {
320 public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
321 if (key.equals(TrendParticipant.KEY_VALUE_TIP_BOX_RELATIVE_POS) && newValue != null) {
322 Session s = Simantics.getSession();
323 ChartResource CHART = s.getService(ChartResource.class);
324 Point2D p = (Point2D) newValue;
325 double[] value = { p.getX(), p.getY() };
326 s.asyncRequest(new SetProperty(getInputResource(), CHART.Chart_valueViewPosition, value, Bindings.DOUBLE_ARRAY));
331 ValueTipBoxPositionListener valueTipBoxPositionListener = new ValueTipBoxPositionListener();
335 IStateListener linkTimeStateListener = new IStateListener() {
337 public void handleStateChange(State state, Object oldValue) {
338 final ChartLinkData newData = (ChartLinkData) linkTimeState.getValue();
339 trendNode.autoscaletime = newData == null || newData.sender == TimeSeriesEditor.this;
341 if ( newData == null || newData.sender==TimeSeriesEditor.this ) return;
342 TrendNode tn = trendNode;
343 HorizRuler hr = tn!=null ? tn.horizRuler : null;
345 ChartLinkData oldData = new ChartLinkData();
348 if ( hr != null && !ObjectUtils.objectEquals(tn.valueTipTime, newData.valueTipTime)) {
349 tn.valueTipTime = newData.valueTipTime;
353 if ( hr != null && (oldData.from!=newData.from || oldData.sx!=newData.sx)) {
355 cvsCtx.getThreadAccess().asyncExec( new Runnable() {
358 boolean b = trendNode.horizRuler.setFromScale(newData.from, newData.sx);
359 trendNode.horizRuler.autoscroll = false;
369 HorizRuler.TimeWindowListener horizRulerListener = new HorizRuler.TimeWindowListener() {
371 public void onNewWindow(double from, double end, double scalex) {
372 final ChartLinkData oldData = (ChartLinkData) linkTimeState.getValue();
373 if (oldData != null) {
374 ChartLinkData data = new ChartLinkData(TimeSeriesEditor.this, from, end, scalex);
375 data.valueTipTime = trendNode.valueTipTime;
376 linkTimeState.setValue( data );
381 class ChassisListener implements IChassisListener {
383 public void chassisClosed(ICanvasChassis sender) {
384 // Prevent deadlock while disposing which using syncExec would result in.
385 final ICanvasContext ctx = cvsCtx;
386 ThreadUtils.asyncExec(ctx.getThreadAccess(), new Runnable() {
390 AWTChassis awt = canvas.getAWTComponent();
392 awt.setCanvasContext(null);
397 canvas.removeChassisListener(ChassisListener.this);
401 ActiveSelectionProvider selectionProvider = new ActiveSelectionProvider();
402 MenuManager menuManager;
404 public TimeSeriesEditor() {
405 log = Logger.getLogger( this.getClass().getName() );
408 boolean isTimeLinked() {
409 if (linkTimeState==null) return false;
410 Boolean isLinked = (Boolean) linkTimeState.getValue();
411 return isLinked != null && isLinked;
415 public void init(IEditorSite site, IEditorInput input) throws PartInitException {
416 super.init(site, input);
418 this.model = Simantics.getSession().syncRequest( new Model( getInputResource() ) );
419 this.chartDataKey = ChartKeys.chartSourceKey(model);
420 } catch (DatabaseException e) {
421 throw new PartInitException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Input " + getInputResource() + " is not part of a model.", e));
426 * Invoke this only from the AWT thread.
429 protected void setCanvasContext(final SWTChassis chassis, final ICanvasContext context) {
430 // Cannot directly invoke SWTChassis.setCanvasContext only because it
431 // needs to be invoked in the SWT thread and AWTChassis.setCanvasContext in the
432 // AWT thread, but directly invoking SWTChassis.setCanvasContext would call both
433 // in the SWT thread which would cause synchronous scheduling of AWT
434 // runnables which is always a potential source of deadlocks.
435 chassis.getAWTComponent().setCanvasContext(context);
436 SWTUtils.asyncExec(chassis, new Runnable() {
439 if (!chassis.isDisposed())
440 // For AWT, this is a no-operation.
441 chassis.setCanvasContext(context);
447 public void createPartControl(Composite parent) {
448 display = parent.getDisplay();
449 swt = SWTThread.getThreadAccess(display);
451 // Must have a project to attach to, otherwise the editor is useless.
452 project = Simantics.peekProject();
453 if (project == null) {
454 errorText = new Text(parent, SWT.NONE);
455 errorText.setText("No project is open.");
456 errorText.setEditable(false);
460 // Create the canvas context here before finishing createPartControl
461 // to give anybody requiring access to this editor's ICanvasContext
462 // a chance to do their work.
463 // The context can be created in SWT thread without scheduling
464 // to the context thread and having potential deadlocks.
465 // The context is locked here and unlocked after it has been
466 // initialized in the AWT thread.
467 IThreadWorkQueue thread = AWTThread.getThreadAccess();
468 cvsCtx = new CanvasContext(thread);
469 cvsCtx.setLocked(true);
471 final IWorkbenchWindow win = getEditorSite().getWorkbenchWindow();
472 final IWorkbenchPage page = getEditorSite().getPage();
474 canvas = new SWTChassis(parent, SWT.NONE);
475 canvas.populate(parameter -> {
477 canvas.addChassisListener(new ChassisListener());
478 initializeCanvas(canvas, cvsCtx, win, page);
483 ICommandService service = (ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class);
484 Command command = service.getCommand( LinkTimeHandler.COMMAND_ID );
485 linkTimeState = command.getState( LinkTimeHandler.STATE_ID );
486 if ( linkTimeState != null ) linkTimeState.addListener( linkTimeStateListener );
490 // Start tracking editor input validity.
491 activateValidation();
493 // Provide input as selection for property page.
494 selectionProvider.setSelection( new StructuredSelection(getInputResource()) );
495 getSite().setSelectionProvider( selectionProvider );
498 protected void initializeCanvas(final SWTChassis chassis, CanvasContext cvsCtx, IWorkbenchWindow window, IWorkbenchPage page) {
499 // Initialize canvas context
500 TrendSpec nodata = new TrendSpec();
502 cvsCtx = TrendInitializer.defaultInitializeCanvas(cvsCtx, null, null, null, nodata);
504 tp = cvsCtx.getAtMostOneItemOfClass(TrendParticipant.class);
507 IContextService contextService = (IContextService) getSite().getService(IContextService.class);
508 contextUtil = new ContextUtil(contextService, swt);
511 cvsCtx.add( new SubscriptionDropParticipant( getInputResource() ) );
512 cvsCtx.add( new KeyToCommand( ChartKeyBindings.DEFAULT_BINDINGS ) );
513 cvsCtx.add( new ChartPasteHandler2(getInputResource().get()) );
514 cvsCtx.add(contextUtil);
516 // Context management
517 cvsCtx.add(new SGFocusParticipant(canvas, "org.simantics.charts.editor.context"));
519 stepListener = new StepListener( tp );
520 trendNode = tp.getTrend();
521 trendNode.titleNode.remove();
522 trendNode.titleNode = null;
525 trendNode.horizRuler.listener = horizRulerListener;
527 final ChartLinkData linkTime = (ChartLinkData) linkTimeState.getValue();
528 if (linkTime!=null) trendNode.horizRuler.setFromEnd(linkTime.from, linkTime.sx);
530 // Handle mouse moved event after TrendParticipant.
531 // This handler forwards trend.mouseHoverTime to linkTimeState
532 cvsCtx.getEventHandlerStack().add( new IEventHandler() {
535 public int getEventMask() {
536 return EventTypes.MouseMovedMask | EventTypes.MouseClickMask | EventTypes.CommandMask | EventTypes.KeyPressed;
540 public boolean handleEvent(Event e) {
542 // System.out.println("LinkEventHandler: "+e);
543 ChartLinkData oldData = (ChartLinkData) linkTimeState.getValue();
545 ChartLinkData newData = new ChartLinkData();
547 if (!newData.equals(oldData)) {
548 // System.out.println("Sending new link-data");
549 linkTimeState.setValue( newData );
555 canvas.getHintContext().setHint( SWTChassis.KEY_EDITORPART, this);
556 canvas.getHintContext().setHint( SWTChassis.KEY_WORKBENCHPAGE, page);
557 canvas.getHintContext().setHint( SWTChassis.KEY_WORKBENCHWINDOW, window);
559 // Canvas context is initialized, unlock it now to allow rendering.
560 cvsCtx.setLocked(false);
562 setCanvasContext(chassis, cvsCtx);
564 cvsCtx.getEventHandlerStack().add(new IEventHandler() {
566 public boolean handleEvent(Event e) {
567 MouseButtonReleasedEvent event = (MouseButtonReleasedEvent) e;
568 if (event.button != MouseEvent.RIGHT_BUTTON)
572 SWTDPIUtil.downscaleSwt((int) event.screenPosition.getX()),
573 SWTDPIUtil.downscaleSwt((int) event.screenPosition.getY()));
574 SWTUtils.asyncExec(chassis, () -> {
575 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);