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 ) );
590 if (chartData.run != null) {
591 milestoneListener = new MilestoneSpecListener();
592 milestoneQuery = new MilestoneSpecQuery( chartData.run );
593 getSession().asyncRequest( milestoneQuery, milestoneListener );
596 // IMPORTANT: Only after preinitializing chartData, start tracking chart configuration
597 trackChartConfiguration();
600 // Write changes to TrendSpec.viewProfile.valueViewPosition[XY]
601 // back to the graph database.
602 cvsCtx.getHintStack().addHintListener(valueTipBoxPositionListener);
605 private void addPopupMenu() {
606 menuManager = new MenuManager("Time Series Editor", CONTEXT_MENU_ID);
607 menuManager.setRemoveAllWhenShown(true);
608 Menu menu = menuManager.createContextMenu(canvas);
609 canvas.setMenu(menu);
610 getEditorSite().registerContextMenu(menuManager.getId(), menuManager, selectionProvider);
612 // Add support for some built-in actions in the context menu.
613 menuManager.addMenuListener(new IMenuListener() {
615 public void menuAboutToShow(IMenuManager manager) {
616 // Not initialized yet, prevent NPE.
617 TrendNode trendNode = TimeSeriesEditor.this.trendNode;
618 TrendParticipant tp = TimeSeriesEditor.this.tp;
619 if (trendNode == null || tp == null)
622 TrendSpec trendSpec = trendNode.getTrendSpec();
623 ItemNode hoverItem = tp.hoveringItem;
624 if (hoverItem != null && hoverItem.item != null) {
625 Resource component = resolveReferencedComponent(getResourceInput(), hoverItem.item.variableId);
626 if (component != null) {
627 manager.add(new PerformDefaultAction("Show Referenced Component", canvas, component));
630 Resource chart = TimeSeriesEditor.this.getInputResource();
631 if ( chart != null ) {
633 Resource chartItem = getSession().sync( new FindChartItemForTrendItem(chart, hoverItem.item) );
634 if (chartItem != null) {
635 manager.add(new HideItemsAction("Hide Item", true, Collections.singletonList(chartItem)));
636 manager.add(new Separator());
637 manager.add(new PropertiesAction("Item Properties", canvas, chartItem));
638 manager.add(new PropertiesAction("Chart Properties", canvas, chart));
640 } catch (DatabaseException e) {
641 Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed to resolve context menu items.", e));
645 boolean hairlineMovementAllowed =
646 !(trendSpec.experimentIsRunning &&
647 trendSpec.viewProfile.trackExperimentTime);
649 Resource chart = TimeSeriesEditor.this.getInputResource();
650 manager.add(new Separator());
651 manager.add(new MoveHairlineAction(
652 "Move Hairline Here",
654 hairlineMovementAllowed,
656 trendNode.mouseHoverTime
658 manager.add(new MoveHairlineAction(
659 "Move Hairline To Current Time",
661 hairlineMovementAllowed,
663 trendNode.horizRuler.getItemEndTime(),
666 manager.add(new TrackExperimentTimeAction(
667 "Hairline Tracks Current Time",
669 trendSpec.viewProfile.trackExperimentTime));
670 manager.add(new Separator());
671 manager.add(new SendCommandAction("Zoom to Fit", IMG_ZOOM_TO_FIT, cvsCtx, Commands.ZOOM_TO_FIT));
672 manager.add(new SendCommandAction("Zoom to Fit Horizontally", IMG_ZOOM_TO_FIT_HORIZ, cvsCtx, Commands.ZOOM_TO_FIT_HORIZ));
673 manager.add(new SendCommandAction("Zoom to Fit Vertically", IMG_ZOOM_TO_FIT_VERT, cvsCtx, Commands.ZOOM_TO_FIT_VERT));
674 manager.add(new SendCommandAction("Autoscale Chart", IMG_AUTOSCALE, cvsCtx, Commands.AUTOSCALE));
675 manager.add(new Separator());
676 manager.add(new PropertiesAction("Chart Properties", canvas, chart));
682 protected Resource resolveReferencedComponent(IResourceEditorInput resourceInput, final String variableId) {
684 return getSession().sync(new UniqueRead<Resource>() {
686 public Resource perform(ReadGraph graph) throws DatabaseException {
687 Variable configuration = Variables.getConfigurationContext(graph, getInputResource());
688 RVI rvi = RVI.fromResourceFormat(graph, variableId);
689 rvi = new RVIBuilder(rvi).removeFromFirstRole(Role.PROPERTY).toRVI();
692 Variable var = rvi.resolve(graph, configuration);
693 return var.getPossibleRepresents(graph);
696 } catch (DatabaseException e) {
697 ErrorLogger.defaultLogError(e);
702 private void showPopup(Point p) {
703 menuManager.getMenu().setLocation(p);
704 menuManager.getMenu().setVisible(true);
707 private void trackChartConfiguration() {
708 getSession().asyncRequest(new TrendSpecQuery( uniqueChartEditorId, getInputResource() ), new TrendSpecListener());
709 getSession().asyncRequest(new ActiveRunQuery( uniqueChartEditorId, getInputResource() ), new ActiveRunListener());
713 public void setFocus() {
714 if (errorText != null)
715 errorText.setFocus();
721 public void dispose() {
722 if (disposed == true) return;
725 if (trendNode!=null && trendNode.horizRuler!=null) {
726 trendNode.horizRuler.listener = null;
729 if ( linkTimeState != null ) linkTimeState.removeListener( linkTimeStateListener );
731 canvas.getHintContext().removeHint( SWTChassis.KEY_EDITORPART );
732 canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHPAGE );
733 canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHWINDOW );
735 if ( chartPreferenceNode!= null ) {
736 chartPreferenceNode.removePreferenceChangeListener( preferenceListener );
739 MilestoneSpecListener ml = milestoneListener;
740 if (ml!=null) ml.dispose();
742 if (project != null) {
743 project.removeHintListener(chartDataListener);
746 if (chartData != null) {
747 if (chartData.datasource!=null)
748 chartData.datasource.removeListener( stepListener );
749 if (chartData.experiment!=null)
750 chartData.experiment.removeListener( experimentStateListener );
751 chartData.readFrom( null );
758 * @param data new data or null
759 * @param newSpec new spec or null
762 @SuppressWarnings("unused")
763 public void setInput(ChartData data, TrendSpec newSpec) {
764 boolean doLayout = false;
766 // Disregard input if it is not for this chart's containing model.
767 if (data != null && data.model != null && !data.model.equals(model))
770 // Accommodate Datasource changes
772 Datasource oldDatasource = chartData==null?null:chartData.datasource;
773 Datasource newDatasource = data==null?null:data.datasource;
774 //if ( !ObjectUtils.objectEquals(oldDatasource, newDatasource) )
776 if (oldDatasource!=null) oldDatasource.removeListener( stepListener );
777 if (newDatasource!=null) newDatasource.addListener( stepListener );
782 IExperiment oldExperiment = chartData==null?null:chartData.experiment;
783 IExperiment newExperiment = data==null?null:data.experiment;
784 //if ( !ObjectUtils.objectEquals(oldExperiment, newExperiment) )
786 if (oldExperiment!=null) oldExperiment.removeListener( experimentStateListener );
787 if (newExperiment!=null) newExperiment.addListener( experimentStateListener );
791 // Accommodate Historian changes
793 HistoryManager oldHistorian = trendNode.historian==null?null:trendNode.historian;
794 HistoryManager newHistorian = data==null?null:data.history;
795 Collector newCollector = data==null?null:data.collector;
796 // if ( !ObjectUtils.objectEquals(oldHistorian, newHistorian) )
798 if (newHistorian instanceof FileHistory) {
799 FileHistory fh = (FileHistory) newHistorian;
800 System.out.println("History = "+fh.getWorkarea());
802 trendNode.setHistorian( newHistorian, newCollector );
803 doLayout |= trendNode.autoscale(true, true) | !ObjectUtils.objectEquals(oldHistorian, newHistorian);
806 // Accommodate TrendSpec changes
807 TrendSpec oldSpec = trendNode.getTrendSpec();
808 if ( !newSpec.equals(oldSpec) ) {
809 trendNode.setTrendSpec( newSpec==null?TrendSpec.EMPTY:newSpec );
815 Resource newExperimentResource = data==null ? null : data.run;
816 Resource oldExperimentResource = this.chartData == null ? null : this.chartData.run;
820 if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {
822 // Dispose old listener & Query
823 if (milestoneListener!=null) {
824 milestoneListener.dispose();
825 milestoneListener = null;
827 if (milestoneQuery!=null) {
828 milestoneQuery = null;
831 trendNode.setMilestones( MilestoneSpec.EMPTY );
833 if (newExperimentResource != null) {
834 milestoneListener = new MilestoneSpecListener();
835 milestoneQuery = new MilestoneSpecQuery( newExperimentResource );
836 Simantics.getSession().asyncRequest( milestoneQuery, milestoneListener );
842 if (doLayout) trendNode.layout();
843 this.chartData.readFrom( data );
846 if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {
847 resetViewAfterDataChange();
852 class ActiveRunListener implements SyncListener<Resource> {
854 public void exception(ReadGraph graph, Throwable throwable) {
855 ErrorLogger.defaultLogError(throwable);
856 ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());
859 public void execute(ReadGraph graph, final Resource run) throws DatabaseException {
861 SimulationResource SIMU = SimulationResource.getInstance(graph);
862 Variable var = Variables.getVariable(graph, run);
863 IExperiment exp = var.getPossiblePropertyValue(graph, SIMU.Run_iExperiment);
864 ITrendSupport ts = exp.getService(ITrendSupport.class);
866 ts.setChartData(graph);
870 public boolean isDisposed() {
871 return TimeSeriesEditor.this.disposed;
875 class TrendSpecListener implements AsyncListener<TrendSpec> {
877 public void exception(AsyncReadGraph graph, Throwable throwable) {
879 ErrorLogger.defaultLogError(throwable);
880 ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());
883 public void execute(AsyncReadGraph graph, final TrendSpec result) {
884 if (result == null) {
885 log.log(Level.INFO, "Chart configuration removed");
887 log.log(Level.INFO, "Chart configuration updated: " + result);
890 // Reload chart in AWT Thread
891 AWTThread.getThreadAccess().asyncExec(new Runnable() {
895 setInput( chartData, result );
900 public boolean isDisposed() {
901 return TimeSeriesEditor.this.disposed;
905 class MilestoneSpecListener implements AsyncListener<MilestoneSpec> {
906 boolean disposed = false;
908 public void execute(AsyncReadGraph graph, final MilestoneSpec result) {
909 AWTThread.INSTANCE.asyncExec(new Runnable() {
911 trendNode.setMilestones(result);
916 public void exception(AsyncReadGraph graph, Throwable throwable) {
921 public boolean isDisposed() {
925 public void dispose() {
931 private void trackPreferences() {
932 chartPreferenceNode = InstanceScope.INSTANCE.getNode( "org.simantics.charts" );
933 chartPreferenceNode.addPreferenceChangeListener( preferenceListener );
934 long redrawInterval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL);
935 long autoscaleInterval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL);
936 setInterval(redrawInterval, autoscaleInterval);
938 String timeFormat = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT);
939 TimeFormat tf = TimeFormat.valueOf( timeFormat );
940 if (tf!=null) setTimeFormat( tf );
942 Boolean drawSamples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES);
943 setDrawSamples(drawSamples);
945 String valueFormat = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT);
946 ValueFormat vf = ValueFormat.valueOf( valueFormat );
947 if (vf!=null) setValueFormat( vf );
949 String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT);
950 ItemPlacement ip = ItemPlacement.valueOf(s);
951 if (trendNode!=null) trendNode.itemPlacement = ip;
953 String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY);
954 String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY);
955 LineQuality q1 = LineQuality.valueOf(s1);
956 LineQuality q2 = LineQuality.valueOf(s2);
957 if (trendNode!=null) trendNode.quality.textQuality = q1;
958 if (trendNode!=null) trendNode.quality.lineQuality = q2;
962 private void setInterval(long redrawInterval, long autoscaleInterval) {
963 redrawInterval = Math.max(1, redrawInterval);
964 long pulse = Math.min(50, redrawInterval);
965 pulse = Math.min(pulse, autoscaleInterval);
966 IHintContext h = canvas.getCanvasContext().getDefaultHintContext();
967 h.setHint(TimeParticipant.KEY_TIME_PULSE_INTERVAL, pulse);
968 h.setHint(TrendParticipant.KEY_TREND_DRAW_INTERVAL, redrawInterval);
969 h.setHint(TrendParticipant.KEY_TREND_AUTOSCALE_INTERVAL, autoscaleInterval);
972 private void setDrawSamples(boolean value) {
973 trendNode.drawSamples = value;
978 private void setTimeFormat( TimeFormat tf ) {
979 if (trendNode.timeFormat == tf) return;
980 trendNode.timeFormat = tf;
985 private void setValueFormat( ValueFormat vf ) {
986 if (trendNode.valueFormat == vf) return;
987 trendNode.valueFormat = vf;
992 @SuppressWarnings("rawtypes")
994 public Object getAdapter(Class adapter) {
995 if (adapter == INode.class) {
996 ICanvasContext ctx = cvsCtx;
998 return ctx.getSceneGraph();
1000 if (adapter == IPropertyPage.class)
1001 return new StandardPropertyPage(getSite(), getPropertyPageContexts());
1002 if (adapter == ICanvasContext.class)
1004 return super.getAdapter(adapter);
1007 protected Set<String> getPropertyPageContexts() {
1009 return BrowseContext.getBrowseContextClosure(Simantics.getSession(), Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT));
1010 } catch (DatabaseException e) {
1011 ExceptionUtils.logAndShowError("Failed to load modeled browse contexts for property page, see exception for details.", e);
1012 return Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT);
1017 * Add from, end, (scale x) to argument array
1018 * @param fromEnd array of 2 or 3
1020 public void getFromEnd(ChartLinkData data) {
1022 TrendNode tn = trendNode;
1023 data.valueTipTime = tn.valueTipTime;
1024 HorizRuler hr = tn!=null ? tn.horizRuler : null;
1026 data.from = hr.from;
1028 double len = hr.end-hr.from;
1029 double wid = tn.plot.getWidth();
1030 if ( wid==0.0 ) wid = 0.1;
1035 @SuppressWarnings("unused")
1036 private static boolean doubleEquals(double a, double b) {
1037 if (Double.isNaN(a) && Double.isNaN(b)) return true;
1041 protected void resetViewAfterDataChange() {
1043 CanvasUtils.sendCommand(cvsCtx, Commands.CANCEL);
1044 CanvasUtils.sendCommand(cvsCtx, Commands.AUTOSCALE);