Fixed context menu popup location for HiDPI displays with display zoom
[simantics/platform.git] / bundles / org.simantics.charts / src / org / simantics / charts / editor / TimeSeriesEditor.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2011 Association for Decentralized Information Management in
3  * Industry THTH ry.
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
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.charts.editor;
13
14 import java.awt.geom.Point2D;
15 import java.util.Collections;
16 import java.util.Set;
17 import java.util.UUID;
18 import java.util.logging.Level;
19 import java.util.logging.Logger;
20
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;
141
142 /**
143  * TimeSeriesEditor is an interactive part that draws a time series chart.
144  * 
145  * The configuration model is {@link TrendSpec} which is read through
146  * {@link TrendSpecQuery}. In Simantics Environment the
147  * editor input is {@link ResourceEditorInput}.
148  * 
149  * @author Toni Kalajainen <toni.kalajainen@vtt.fi>
150  * @author Tuukka Lehtonen
151  */
152 public class TimeSeriesEditor extends ResourceEditorPart {      
153
154     ParametrizedRead<IResourceEditorInput, Boolean> INPUT_VALIDATOR =
155         Combinators.compose(
156                 InputValidationCombinators.hasURI(),
157                 InputValidationCombinators.extractInputResource()
158         );
159
160     @Override
161     protected ParametrizedRead<IResourceEditorInput, Boolean> getInputValidator() {
162         return INPUT_VALIDATOR;
163     }
164
165     /**
166      * The root property browse context of the time series editor. A transitive
167      * closure is calculated for this context.
168      */
169     private static String       ROOT_PROPERTY_BROWSE_CONTEXT = ChartResource.URIs.ChartBrowseContext;
170
171     /**
172      * ID of the this editor part extension.
173      */
174     public static final String  ID                  = "org.simantics.charts.editor.timeseries";
175
176     private static final String CONTEXT_MENU_ID     = "#timeSeriesChart";
177
178     private IEclipsePreferences chartPreferenceNode;
179
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");
184
185     IPreferenceChangeListener preferenceListener = new IPreferenceChangeListener() {
186         @Override
187         public void preferenceChange(PreferenceChangeEvent event) {
188             if (disposed) {
189                 System.err.println("Warning: pref change to disposed TimeSeriesEditor");
190                 return;
191             }
192
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 );
198             }
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 );
202             }
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 );
207             }
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 );
212             }
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;
217             }
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;
225             }
226             
227         }
228     };
229
230     /**
231      * The project which this editor is listening to for changes to
232      * {@link ChartKeys.ChartSourceKey keys}.
233      */
234     IProject                    project;
235
236     /**
237      * The model resource containing the input chart resource.
238      */
239     Resource                    model;
240
241     /**
242      * The text widget shown only if there is no IProject available at the time
243      * of editor part creation.
244      */
245     Text                        errorText;
246
247     /**
248      * A unique key for making DB requests chart editor specific without binding
249      * the requests to the editor object itself.
250      */
251     UUID                        uniqueChartEditorId = UUID.randomUUID();
252     Logger                      log;
253     Display                     display;
254     SWTChassis                  canvas;
255     CanvasContext               cvsCtx;
256     TrendParticipant            tp;
257     TrendNode                   trendNode;
258     StepListener                stepListener;
259     MilestoneSpecListener       milestoneListener;
260     MilestoneSpecQuery          milestoneQuery;
261
262     /**
263      * The ChartData instance used by this editor for sourcing data at any given
264      * moment. Project hint instances are copied into this instance.
265      */
266     final ChartData             chartData = new ChartData(null, null, null, null, null, null);
267
268     /**
269      * The ChartSourceKey to match the model this editor was opened for.
270      * @see #model
271      * @see #init(IEditorSite, IEditorInput)
272      */
273     ChartKeys.ChartSourceKey    chartDataKey;
274     
275     
276     /**
277      * Context management utils
278      */
279     protected IThreadWorkQueue           swt;
280     protected ContextUtil                contextUtil;
281
282     class ExperimentStateListener implements IExperimentListener {
283         @Override
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;
289                 if (t != null)
290                     t.setDirty();
291             }
292         }
293     }
294
295     ExperimentStateListener experimentStateListener = new ExperimentStateListener();
296
297     class ChartDataListener extends HintListenerAdapter implements Runnable {
298         @Override
299         public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
300             if (key.equals(chartDataKey)) {
301                 // @Thread any
302                 if (!cvsCtx.isDisposed() && cvsCtx.isAlive()) {
303                     cvsCtx.getThreadAccess().asyncExec(this);
304                 }
305             }
306         }
307         @Override
308         public void run() {
309             // @Thread AWT
310             if (cvsCtx.isDisposed() || !cvsCtx.isAlive()) return;
311             ChartData data = Simantics.getProject().getHint(chartDataKey);
312             setInput( data, trendNode.getTrendSpec() );
313         }
314     }
315
316     ChartDataListener chartDataListener = new ChartDataListener();
317
318     class ValueTipBoxPositionListener extends HintListenerAdapter {
319         @Override
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));
327             }
328         }
329     }
330
331     ValueTipBoxPositionListener valueTipBoxPositionListener = new ValueTipBoxPositionListener();
332
333     // Link-Time
334     State linkTimeState;
335     IStateListener linkTimeStateListener = new IStateListener() {
336                 @Override
337                 public void handleStateChange(State state, Object oldValue) {
338                         final ChartLinkData newData = (ChartLinkData) linkTimeState.getValue();
339                         trendNode.autoscaletime = newData == null || newData.sender == TimeSeriesEditor.this;
340                         
341                         if ( newData == null || newData.sender==TimeSeriesEditor.this ) return;
342                         TrendNode tn = trendNode;
343                         HorizRuler hr = tn!=null ? tn.horizRuler : null;
344
345                         ChartLinkData oldData = new ChartLinkData();
346                         getFromEnd(oldData);
347
348                         if ( hr != null && !ObjectUtils.objectEquals(tn.valueTipTime, newData.valueTipTime)) {
349                                 tn.valueTipTime = newData.valueTipTime;
350                                 tp.setDirty();
351                         }
352                         
353                         if ( hr != null && (oldData.from!=newData.from || oldData.sx!=newData.sx)) {
354                                 
355                         cvsCtx.getThreadAccess().asyncExec( new Runnable() {
356                                         @Override
357                                         public void run() {
358                                                 boolean b = trendNode.horizRuler.setFromScale(newData.from, newData.sx);
359                                                 trendNode.horizRuler.autoscroll = false;
360                                                 if (b) {
361                                                         trendNode.layout();
362                                                         tp.setDirty();
363                                                 }
364                                         }});
365                         }
366                         
367                 }
368     };
369     HorizRuler.TimeWindowListener horizRulerListener = new HorizRuler.TimeWindowListener() {
370                 @Override
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 );
377                         }
378                 }
379         };
380
381     class ChassisListener implements IChassisListener {
382         @Override
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() {
387                 @Override
388                 public void run() {
389                     if (ctx != null) {
390                         AWTChassis awt = canvas.getAWTComponent();
391                         if (awt != null)
392                             awt.setCanvasContext(null);
393                         ctx.dispose();
394                     }
395                 }
396             });
397             canvas.removeChassisListener(ChassisListener.this);
398         }
399     }
400
401     ActiveSelectionProvider     selectionProvider   = new ActiveSelectionProvider();
402     MenuManager                 menuManager;
403
404     public TimeSeriesEditor() {
405         log = Logger.getLogger( this.getClass().getName() );
406     }
407
408     boolean isTimeLinked() {
409         if (linkTimeState==null) return false;
410         Boolean isLinked = (Boolean) linkTimeState.getValue();
411         return isLinked != null && isLinked;
412     }
413
414     @Override
415     public void init(IEditorSite site, IEditorInput input) throws PartInitException {
416         super.init(site, input);
417         try {
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));
422         }
423     }
424     
425     /**
426      * Invoke this only from the AWT thread.
427      * @param context
428      */
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() {
437             @Override
438             public void run() {
439                 if (!chassis.isDisposed())
440                     // For AWT, this is a no-operation.
441                     chassis.setCanvasContext(context);
442             }
443         });
444     }
445
446     @Override
447     public void createPartControl(Composite parent) {
448         display = parent.getDisplay();
449         swt = SWTThread.getThreadAccess(display);
450
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);
457             return;
458         }
459
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);
470
471         final IWorkbenchWindow win = getEditorSite().getWorkbenchWindow();
472         final IWorkbenchPage page = getEditorSite().getPage();
473
474         canvas = new SWTChassis(parent, SWT.NONE);
475         canvas.populate(parameter -> {
476             if (!disposed) {
477                 canvas.addChassisListener(new ChassisListener());
478                 initializeCanvas(canvas, cvsCtx, win, page);
479             }
480         });
481
482         // Link time
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 );
487
488         addPopupMenu();
489
490         // Start tracking editor input validity. 
491         activateValidation();
492
493         // Provide input as selection for property page.
494         selectionProvider.setSelection( new StructuredSelection(getInputResource()) );
495         getSite().setSelectionProvider( selectionProvider );
496     }
497
498     protected void initializeCanvas(final SWTChassis chassis, CanvasContext cvsCtx, IWorkbenchWindow window, IWorkbenchPage page) {
499         // Initialize canvas context
500         TrendSpec nodata = new TrendSpec();
501         nodata.init();
502         cvsCtx = TrendInitializer.defaultInitializeCanvas(cvsCtx, null, null, null, nodata);
503
504         tp = cvsCtx.getAtMostOneItemOfClass(TrendParticipant.class);
505
506         
507         IContextService contextService = (IContextService) getSite().getService(IContextService.class);
508         contextUtil = new ContextUtil(contextService, swt);
509
510         
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);
515         
516         // Context management
517         cvsCtx.add(new SGFocusParticipant(canvas, "org.simantics.charts.editor.context"));
518
519         stepListener = new StepListener( tp );
520         trendNode = tp.getTrend();
521         trendNode.titleNode.remove();
522         trendNode.titleNode = null;
523
524         // Link time
525         trendNode.horizRuler.listener = horizRulerListener;
526         
527         final ChartLinkData linkTime = (ChartLinkData) linkTimeState.getValue();
528         if (linkTime!=null) trendNode.horizRuler.setFromEnd(linkTime.from, linkTime.sx);
529         
530         // Handle mouse moved event after TrendParticipant.
531         // This handler forwards trend.mouseHoverTime to linkTimeState
532         cvsCtx.getEventHandlerStack().add( new IEventHandler() {
533
534                         @Override
535                         public int getEventMask() {
536                                 return EventTypes.MouseMovedMask | EventTypes.MouseClickMask | EventTypes.CommandMask | EventTypes.KeyPressed;
537                         }
538
539                         @Override
540                         public boolean handleEvent(Event e) {
541
542 //                              System.out.println("LinkEventHandler: "+e);
543                                 ChartLinkData oldData = (ChartLinkData) linkTimeState.getValue();
544                                 if (oldData!=null) {
545                                         ChartLinkData newData = new ChartLinkData();
546                                         getFromEnd(newData);
547                                         if (!newData.equals(oldData)) {
548 //                                              System.out.println("Sending new link-data");
549                                                 linkTimeState.setValue( newData );
550                                         }
551                                 }
552                                 return false;
553                         }}, -1);
554         
555         canvas.getHintContext().setHint( SWTChassis.KEY_EDITORPART, this);
556         canvas.getHintContext().setHint( SWTChassis.KEY_WORKBENCHPAGE, page);
557         canvas.getHintContext().setHint( SWTChassis.KEY_WORKBENCHWINDOW, window);
558
559         // Canvas context is initialized, unlock it now to allow rendering.
560         cvsCtx.setLocked(false);
561
562         setCanvasContext(chassis, cvsCtx);
563
564         cvsCtx.getEventHandlerStack().add(new IEventHandler() {
565             @Override
566             public boolean handleEvent(Event e) {
567                 MouseButtonReleasedEvent event = (MouseButtonReleasedEvent) e;
568                 if (event.button != MouseEvent.RIGHT_BUTTON)
569                     return false;
570
571                 Point p = new Point(
572                         SWTDPIUtil.downscaleSwt((int) event.screenPosition.getX()),
573                         SWTDPIUtil.downscaleSwt((int) event.screenPosition.getY()));
574                 SWTUtils.asyncExec(chassis, () -> {
575                     if (!canvas.isDisposed())
576                         showPopup(p);
577                 });
578                 return true;
579             }
580             @Override
581             public int getEventMask() {
582                 return EventTypes.MouseButtonReleasedMask;
583             }
584         }, 1000000);
585
586         // Track data source and preinitialize chartData
587         project.addHintListener(chartDataListener);
588         chartData.readFrom( (ChartData) project.getHint( chartDataKey ) );
589         chartData.reference();
590
591         if (chartData.run != null) {
592             milestoneListener = new MilestoneSpecListener();
593             milestoneQuery = new MilestoneSpecQuery( chartData.run );
594             getSession().asyncRequest( milestoneQuery, milestoneListener );
595         }
596
597         // IMPORTANT: Only after preinitializing chartData, start tracking chart configuration
598         trackChartConfiguration();
599         trackPreferences();
600
601         // Write changes to TrendSpec.viewProfile.valueViewPosition[XY]
602         // back to the graph database.
603         cvsCtx.getHintStack().addHintListener(valueTipBoxPositionListener);
604         }
605
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);
612
613         // Add support for some built-in actions in the context menu.
614         menuManager.addMenuListener(new IMenuListener() {
615             @Override
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)
621                     return;
622
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));
629                     }
630
631                     Resource chart = TimeSeriesEditor.this.getInputResource();
632                     if ( chart != null ) {
633                         try {
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));
640                             }
641                         } catch (DatabaseException e) {
642                             Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed to resolve context menu items.", e));
643                         }
644                     }
645                 } else {
646                     boolean hairlineMovementAllowed =
647                             !(trendSpec.experimentIsRunning &&
648                               trendSpec.viewProfile.trackExperimentTime);
649
650                     Resource chart = TimeSeriesEditor.this.getInputResource();
651                     manager.add(new Separator());
652                     manager.add(new MoveHairlineAction(
653                             "Move Hairline Here",
654                             chart,
655                             hairlineMovementAllowed,
656                             trendNode,
657                             trendNode.mouseHoverTime
658                             ));
659                     manager.add(new MoveHairlineAction(
660                             "Move Hairline To Current Time",
661                             chart,
662                             hairlineMovementAllowed,
663                             trendNode,
664                             trendNode.horizRuler.getItemEndTime(),
665                             Boolean.FALSE
666                             ));
667                     manager.add(new TrackExperimentTimeAction(
668                             "Hairline Tracks Current Time",
669                             chart,
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));
678                 }
679             }
680         });
681     }
682
683     protected Resource resolveReferencedComponent(IResourceEditorInput resourceInput, final String variableId) {
684         try {
685             return getSession().sync(new UniqueRead<Resource>() {
686                 @Override
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();
691                     if (rvi.isEmpty())
692                         return null;
693                     Variable var = rvi.resolve(graph, configuration);
694                     return var.getPossibleRepresents(graph);
695                 }
696             });
697         } catch (DatabaseException e) {
698             ErrorLogger.defaultLogError(e);
699         }
700         return null;
701     }
702
703     private void showPopup(Point p) {
704         menuManager.getMenu().setLocation(p);
705         menuManager.getMenu().setVisible(true);
706     }
707
708     private void trackChartConfiguration() {
709         getSession().asyncRequest(new TrendSpecQuery( uniqueChartEditorId, getInputResource() ), new TrendSpecListener());
710         getSession().asyncRequest(new ActiveRunQuery( uniqueChartEditorId, getInputResource() ), new ActiveRunListener());
711     }
712
713     @Override
714     public void setFocus() {
715         if (errorText != null)
716             errorText.setFocus();
717         else
718             canvas.setFocus();
719     }
720
721     @Override
722     public void dispose() {
723         if (disposed == true) return;
724         disposed = true;
725
726         if (trendNode!=null && trendNode.horizRuler!=null) {
727             trendNode.horizRuler.listener = null;
728         }
729
730         if ( linkTimeState != null ) linkTimeState.removeListener( linkTimeStateListener );
731
732         canvas.getHintContext().removeHint( SWTChassis.KEY_EDITORPART );
733         canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHPAGE );
734         canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHWINDOW );
735
736         if ( chartPreferenceNode!= null ) {
737             chartPreferenceNode.removePreferenceChangeListener( preferenceListener );
738         }
739
740         MilestoneSpecListener ml = milestoneListener;
741         if (ml!=null) ml.dispose();
742
743         if (project != null) {
744             project.removeHintListener(chartDataListener);
745         }
746
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 );
754         }
755
756         super.dispose();
757     }
758
759     /**
760      * @param data new data or null
761      * @param newSpec new spec or null
762      * @thread AWT
763      */
764     @SuppressWarnings("unused")
765     public void setInput(ChartData data, TrendSpec newSpec) {
766         boolean doLayout = false;
767
768         // Disregard input if it is not for this chart's containing model.
769         if (data != null && data.model != null && !data.model.equals(model))
770             data = null;
771
772         // Accommodate Datasource changes
773         Datasource: {
774                 Datasource oldDatasource = chartData==null?null:chartData.datasource;
775                 Datasource newDatasource = data==null?null:data.datasource;
776                 //if ( !ObjectUtils.objectEquals(oldDatasource, newDatasource) ) 
777                 {
778                         if (oldDatasource!=null) oldDatasource.removeListener( stepListener );
779                         if (newDatasource!=null) newDatasource.addListener( stepListener );
780                 }
781         }
782
783         Experiment: {
784             IExperiment oldExperiment = chartData==null?null:chartData.experiment;
785             IExperiment newExperiment = data==null?null:data.experiment;
786             //if ( !ObjectUtils.objectEquals(oldExperiment, newExperiment) ) 
787             {
788                 if (oldExperiment!=null) oldExperiment.removeListener( experimentStateListener );
789                 if (newExperiment!=null) newExperiment.addListener( experimentStateListener );
790             }
791         }
792
793         // Accommodate Historian changes
794         Historian: {
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) ) 
799                 {
800                         if (newHistorian instanceof FileHistory) {
801                                 FileHistory fh = (FileHistory) newHistorian;
802                                 System.out.println("History = "+fh.getWorkarea());
803                         }
804                         trendNode.setHistorian( newHistorian, newCollector );
805                         doLayout |= trendNode.autoscale(true, true) | !ObjectUtils.objectEquals(oldHistorian, newHistorian);
806                 }
807
808                 // Accommodate TrendSpec changes
809                 TrendSpec oldSpec = trendNode.getTrendSpec();
810                 if ( !newSpec.equals(oldSpec) ) {
811                     trendNode.setTrendSpec( newSpec==null?TrendSpec.EMPTY:newSpec );
812                     doLayout = true;
813                 }
814                 
815         }
816
817         Resource newExperimentResource = data==null ? null : data.run;
818         Resource oldExperimentResource = this.chartData == null ? null : this.chartData.run;
819         
820         // Track milestones
821         Milestones: {
822                 if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {
823
824                         // Dispose old listener & Query
825                         if (milestoneListener!=null) {
826                                 milestoneListener.dispose();
827                                 milestoneListener = null;
828                         }
829                         if (milestoneQuery!=null) {
830                                 milestoneQuery = null;
831                         }
832
833                         trendNode.setMilestones( MilestoneSpec.EMPTY );
834                         
835                         if (newExperimentResource != null) {
836                                 milestoneListener = new MilestoneSpecListener();
837                                 milestoneQuery = new MilestoneSpecQuery( newExperimentResource );
838                                 Simantics.getSession().asyncRequest( milestoneQuery, milestoneListener );
839                         }
840                 }
841                 
842         }
843
844         if (doLayout) trendNode.layout();
845         this.chartData.dereference();
846         this.chartData.readFrom( data );
847         this.chartData.reference();
848         tp.setDirty();
849         
850         if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {
851                 resetViewAfterDataChange();
852         }
853         
854     }
855
856     class ActiveRunListener implements SyncListener<Resource> {
857         @Override
858         public void exception(ReadGraph graph, Throwable throwable) {
859             ErrorLogger.defaultLogError(throwable);
860             ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());
861         }
862         @Override
863         public void execute(ReadGraph graph, final Resource run) throws DatabaseException {
864             if(run != null) {
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;
869                 if (ts != null)
870                     ts.setChartData(graph);
871             }
872         }
873         @Override
874         public boolean isDisposed() {
875             return TimeSeriesEditor.this.disposed;
876         }
877     }
878     
879     class TrendSpecListener implements AsyncListener<TrendSpec> {
880         @Override
881         public void exception(AsyncReadGraph graph, Throwable throwable) {
882                 
883             ErrorLogger.defaultLogError(throwable);
884             ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());
885         }
886         @Override
887         public void execute(AsyncReadGraph graph, final TrendSpec result) {
888             if (result == null) {
889                 log.log(Level.INFO, "Chart configuration removed");
890             } else {
891                 log.log(Level.INFO, "Chart configuration updated: " + result);
892             }
893
894             // Reload chart in AWT Thread
895             AWTThread.getThreadAccess().asyncExec(new Runnable() {
896                 @Override
897                 public void run() {
898                     if (!disposed)
899                         setInput( chartData, result );
900                 }
901             });
902         }
903         @Override
904         public boolean isDisposed() {
905             return TimeSeriesEditor.this.disposed;
906         }
907     }
908     
909     class MilestoneSpecListener implements AsyncListener<MilestoneSpec> {
910         boolean disposed = false;
911                 @Override
912                 public void execute(AsyncReadGraph graph, final MilestoneSpec result) {
913                         AWTThread.INSTANCE.asyncExec(new Runnable() {
914                                 public void run() {
915                                         trendNode.setMilestones(result);
916                                 }});
917                 }
918
919                 @Override
920                 public void exception(AsyncReadGraph graph, Throwable throwable) {
921                         
922                 }
923
924                 @Override
925                 public boolean isDisposed() {
926                         return disposed;
927                 }
928                 
929                 public void dispose() {
930                         disposed = true;
931                 }
932         
933     }
934
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);
941         
942         String timeFormat = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT); 
943         TimeFormat tf = TimeFormat.valueOf( timeFormat );
944         if (tf!=null) setTimeFormat( tf );
945         
946         Boolean drawSamples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES);
947         setDrawSamples(drawSamples);
948
949         String valueFormat = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT);
950         ValueFormat vf = ValueFormat.valueOf( valueFormat );
951         if (vf!=null) setValueFormat( vf );
952         
953         String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT);
954         ItemPlacement ip = ItemPlacement.valueOf(s);
955         if (trendNode!=null) trendNode.itemPlacement = ip;
956         
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;
963         
964     }
965
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);        
974     }
975
976     private void setDrawSamples(boolean value) {
977         trendNode.drawSamples = value;
978         trendNode.layout();
979         tp.setDirty();
980     }
981
982     private void setTimeFormat( TimeFormat tf ) {
983         if (trendNode.timeFormat == tf) return;
984         trendNode.timeFormat = tf;
985         trendNode.layout();
986         tp.setDirty();
987     }
988
989     private void setValueFormat( ValueFormat vf ) {
990         if (trendNode.valueFormat == vf) return;
991         trendNode.valueFormat = vf;
992         trendNode.layout();
993         tp.setDirty();
994     }
995
996     @SuppressWarnings("unchecked")
997     @Override
998     public <T> T getAdapter(Class<T> adapter) {
999         if (adapter == INode.class) {
1000             ICanvasContext ctx = cvsCtx;
1001             if (ctx != null)
1002                 return (T) ctx.getSceneGraph();
1003         }
1004         if (adapter == IPropertyPage.class)
1005             return (T) new StandardPropertyPage(getSite(), getPropertyPageContexts());
1006         if (adapter == ICanvasContext.class)
1007             return (T) cvsCtx;
1008         return super.getAdapter(adapter);
1009     }
1010
1011     protected Set<String> getPropertyPageContexts() {
1012         try {
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);
1017         }
1018     }
1019
1020     /**
1021      * Add from, end, (scale x) to argument array
1022      * @param fromEnd array of 2 or 3
1023      */
1024     public void getFromEnd(ChartLinkData data) {
1025         data.sender = this;
1026         TrendNode tn = trendNode;
1027         data.valueTipTime = tn.valueTipTime;
1028         HorizRuler hr = tn!=null ? tn.horizRuler : null;
1029         if ( hr != null ) {
1030                 data.from = hr.from;
1031                 data.end = hr.end;
1032                         double len = hr.end-hr.from; 
1033                         double wid = tn.plot.getWidth();
1034                         if ( wid==0.0 ) wid = 0.1;
1035                         data.sx = len/wid;
1036         }
1037     }
1038
1039     @SuppressWarnings("unused")
1040     private static boolean doubleEquals(double a, double b) {
1041         if (Double.isNaN(a) && Double.isNaN(b)) return true;
1042         return a==b;
1043     }
1044
1045     protected void resetViewAfterDataChange() {
1046         
1047         CanvasUtils.sendCommand(cvsCtx, Commands.CANCEL);
1048         CanvasUtils.sendCommand(cvsCtx, Commands.AUTOSCALE);
1049         
1050     }
1051
1052 }