]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/diagramEditor/PopulateElementDropParticipant.java
Configurable connection crossing styles
[simantics/platform.git] / bundles / org.simantics.modeling.ui / src / org / simantics / modeling / ui / diagramEditor / PopulateElementDropParticipant.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2018 Association for Decentralized Information Management
3  * in 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  *     Semantum Oy - gitlab #215
12  *******************************************************************************/
13 package org.simantics.modeling.ui.diagramEditor;
14
15 import java.awt.Point;
16 import java.awt.datatransfer.Transferable;
17 import java.awt.datatransfer.UnsupportedFlavorException;
18 import java.awt.dnd.DnDConstants;
19 import java.awt.dnd.DropTargetDragEvent;
20 import java.awt.dnd.DropTargetDropEvent;
21 import java.awt.dnd.DropTargetEvent;
22 import java.awt.geom.AffineTransform;
23 import java.awt.geom.Point2D;
24 import java.io.IOException;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.List;
28 import java.util.Set;
29 import java.util.stream.Collectors;
30
31 import org.eclipse.core.runtime.IAdaptable;
32 import org.eclipse.core.runtime.IStatus;
33 import org.eclipse.core.runtime.Status;
34 import org.eclipse.jface.viewers.IStructuredSelection;
35 import org.eclipse.osgi.util.NLS;
36 import org.eclipse.swt.widgets.Shell;
37 import org.eclipse.ui.IWorkbenchPartSite;
38 import org.simantics.Simantics;
39 import org.simantics.db.ReadGraph;
40 import org.simantics.db.RequestProcessor;
41 import org.simantics.db.Resource;
42 import org.simantics.db.Session;
43 import org.simantics.db.common.request.PossibleIndexRoot;
44 import org.simantics.db.common.request.ResourceRead2;
45 import org.simantics.db.common.utils.NameUtils;
46 import org.simantics.db.exception.DatabaseException;
47 import org.simantics.db.layer0.request.IsLinkedTo;
48 import org.simantics.db.layer0.util.Layer0Utils;
49 import org.simantics.db.layer0.variable.Variable;
50 import org.simantics.db.layer0.variable.Variables;
51 import org.simantics.db.request.Read;
52 import org.simantics.db.request.Write;
53 import org.simantics.db.service.SerialisationSupport;
54 import org.simantics.diagram.adapter.GraphToDiagramSynchronizer;
55 import org.simantics.diagram.content.Change;
56 import org.simantics.diagram.content.DiagramContentChanges;
57 import org.simantics.diagram.content.DiagramContentTracker;
58 import org.simantics.diagram.stubs.DiagramResource;
59 import org.simantics.diagram.synchronization.runtime.DiagramSelectionUpdater;
60 import org.simantics.diagram.ui.DiagramModelHints;
61 import org.simantics.diagram.ui.ElementClassTransferable;
62 import org.simantics.diagram.ui.ElementClassTransferable.ResourceElementClassTransferData;
63 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
64 import org.simantics.g2d.diagram.DiagramHints;
65 import org.simantics.g2d.diagram.DiagramUtils;
66 import org.simantics.g2d.diagram.IDiagram;
67 import org.simantics.g2d.diagram.handler.PickContext;
68 import org.simantics.g2d.diagram.handler.PickRequest;
69 import org.simantics.g2d.diagram.participant.AbstractDiagramParticipant;
70 import org.simantics.g2d.dnd.DnDHints;
71 import org.simantics.g2d.dnd.ElementClassDragItem;
72 import org.simantics.g2d.dnd.IDnDContext;
73 import org.simantics.g2d.dnd.IDragItem;
74 import org.simantics.g2d.dnd.IDropTargetParticipant;
75 import org.simantics.g2d.element.ElementClass;
76 import org.simantics.g2d.element.ElementHints;
77 import org.simantics.g2d.element.ElementUtils;
78 import org.simantics.g2d.element.IElement;
79 import org.simantics.g2d.participant.TransformUtil;
80 import org.simantics.modeling.ModelingResources;
81 import org.simantics.modeling.ui.Activator;
82 import org.simantics.modeling.ui.diagramEditor.dnd.DropSuggestion;
83 import org.simantics.modeling.ui.diagramEditor.dnd.DropSuggestions;
84 import org.simantics.scenegraph.g2d.snap.ISnapAdvisor;
85 import org.simantics.structural.stubs.StructuralResource2;
86 import org.simantics.ui.dnd.LocalObjectTransfer;
87 import org.simantics.ui.dnd.LocalObjectTransferable;
88 import org.simantics.ui.selection.WorkbenchSelectionElement;
89 import org.simantics.utils.datastructures.hints.IHintContext;
90 import org.simantics.utils.datastructures.hints.IHintContext.Key;
91 import org.simantics.utils.logging.TimeLogger;
92 import org.simantics.utils.strings.EString;
93 import org.simantics.utils.ui.SWTUtils;
94 import org.simantics.utils.ui.dialogs.ShowError;
95 import org.simantics.utils.ui.workbench.WorkbenchUtils;
96 import org.slf4j.Logger;
97 import org.slf4j.LoggerFactory;
98
99 /**
100  * This participant populates Elements from ElementClass-resources drops
101  */
102 public class PopulateElementDropParticipant extends AbstractDiagramParticipant implements IDropTargetParticipant {
103
104     private static final Logger LOGGER = LoggerFactory.getLogger(PopulateElementDropParticipant.class);
105
106     /**
107      * List of {@link DropSuggestion} instances that need to be applied to be model
108      * before the drop can commence. Used with the hint context from
109      * {@link IDnDContext#getHints()}.
110      */
111     private static final Key KEY_SUGGESTIONS = new IHintContext.KeyOf(List.class);
112
113     @Dependency PickContext pickContext;
114     @Dependency TransformUtil transformUtil;
115
116     protected GraphToDiagramSynchronizer synchronizer;
117     protected IWorkbenchPartSite partSite;
118
119     public PopulateElementDropParticipant(GraphToDiagramSynchronizer synchronizer) {
120         this(synchronizer, null);
121     }
122
123     public PopulateElementDropParticipant(GraphToDiagramSynchronizer synchronizer, IWorkbenchPartSite partSite) {
124         this.synchronizer = synchronizer;
125         this.partSite = partSite;
126     }
127
128     @Override
129     public void dragEnter(DropTargetDragEvent dtde, IDnDContext dp) {
130         if (diagram == null)
131             return;
132
133         Transferable tr = dtde.getTransferable();
134         if (tr.isDataFlavorSupported(LocalObjectTransferable.FLAVOR)) {
135             Object obj = null;
136
137             // This must be done to have SWT transfer set the source data
138             try {
139                 obj = tr.getTransferData(LocalObjectTransferable.FLAVOR);
140                 // System.out.println("GOT FROM AWT: " + obj);
141             } catch (UnsupportedFlavorException | IOException e) {
142                 LOGGER.error("Could not get AWT transferable data", e); //$NON-NLS-1$
143             }
144
145             // Check SWT
146             if (!(obj instanceof IStructuredSelection)) {
147                 obj = LocalObjectTransfer.getTransfer().getObject();
148                 // System.out.println("GOT FROM SWT: " + obj);
149             }
150
151             if (obj instanceof IStructuredSelection) {
152                 IStructuredSelection sel = (IStructuredSelection) obj;
153                 if (!sel.isEmpty()) {
154                     for (Object elm : sel.toList()) {
155                         if (elm instanceof IAdaptable) {
156                             ElementClass ec = (ElementClass) ((IAdaptable) elm).getAdapter(ElementClass.class);
157                             if (ec != null) {
158                                 dp.add(new ElementClassDragItem(ec));
159                             } else {
160                                 Resource r = (Resource) ((IAdaptable) elm).getAdapter(Resource.class);
161                                 if (r != null) {
162                                     try {
163                                         Object errorOrSymbolResource = validateDrag(synchronizer.getSession(), r,
164                                                 diagram.<Resource> getHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE),
165                                                 dp.getHints());
166                                         if (errorOrSymbolResource instanceof Resource) {
167                                             Resource symbol = (Resource) errorOrSymbolResource;
168                                             ElementClassDragItem item = new ElementClassDragItem(synchronizer.getNodeClass(symbol));
169                                             item.getHintContext().setHint(ElementHints.KEY_TRANSFORM, AffineTransform.getScaleInstance(1, 1));
170                                             item.getHintContext().setHint(ElementHints.KEY_OBJECT, symbol);
171                                             dp.add(item);
172                                         }
173                                     } catch (DatabaseException e) {
174                                         // Ignore node-class retrieval failures, so only log as debug
175                                         LOGGER.debug("Could not retrieve node class for dropped symbol", e); //$NON-NLS-1$
176                                     }
177                                 }
178                             }
179                         }
180                     }
181
182                     // Let the default logic handle out how many columns to use.
183                     dp.getHints().removeHint(DnDHints.KEY_DND_GRID_COLUMNS);
184                 }
185             }
186
187             return;
188         }
189
190         if (tr.isDataFlavorSupported(ElementClassTransferable.FLAVOR)) {
191             ResourceElementClassTransferData dada;
192             try {
193                 dada = (ResourceElementClassTransferData) tr.getTransferData(ElementClassTransferable.FLAVOR);
194             } catch (UnsupportedFlavorException e) {
195                 throw new Error(e);
196             } catch (IOException e) {
197                 throw new Error(e);
198             }
199             Session s = synchronizer.getSession();
200             try {
201                 for (String rid : dada.elementClassResourceRandomAccessReference) {
202                     SerialisationSupport support = s.getService(SerialisationSupport.class);
203                     Resource r = support.getResource(Long.parseLong(rid));
204                     dp.add(new ElementClassDragItem(synchronizer.getNodeClass(r)));
205                 }
206             } catch (DatabaseException e) {
207                 throw new RuntimeException(e);
208             }
209
210             return;
211         }
212     }
213
214     private Object validateDrag(RequestProcessor processor, final Resource draggedResource, final Resource dropTarget, IHintContext dndHints) throws DatabaseException {
215         return processor.syncRequest((Read<Object>) graph -> {
216             List<DropSuggestion> suggestions = dndHints.getHint(KEY_SUGGESTIONS);
217             if (suggestions == null) {
218                 suggestions = new ArrayList<>();
219                 dndHints.setHint(KEY_SUGGESTIONS, suggestions);
220             }
221
222             //System.out.println("dragged resource: " + draggedResource);
223             //System.out.println("drop target resource: " + dropTarget);
224             Resource sourceRoot = graph.syncRequest(new PossibleIndexRoot(draggedResource));
225             Resource targetRoot = graph.syncRequest(new PossibleIndexRoot(dropTarget));
226             //System.out.println("source model: " + sourceRoot);
227             //System.out.println("target model: " + targetRoot);
228
229             // Prevent dragging data from one source model to another.
230             // If source is not part of any model, everything is okay.
231             if (sourceRoot != null && !graph.syncRequest(new IsLinkedTo(targetRoot, sourceRoot))) {
232                 // Prevent instantiation from source roots that are already dependent on the target root.
233                 // This would form a dependency cycle.
234                 if (graph.syncRequest(new IsLinkedTo(sourceRoot, targetRoot))) {
235                     return NLS.bind("Cannot instantiate {0} into namespace {1}. The source namespace ({2}) is already linked to the target namespace. Linking the target to the source would form a dependency cycle.", //$NON-NLS-1$
236                             new Object[] {
237                                     NameUtils.getSafeName(graph, draggedResource),
238                                     NameUtils.getURIOrSafeNameInternal(graph, targetRoot),
239                                     NameUtils.getURIOrSafeNameInternal(graph, sourceRoot)
240                     });
241                 }
242
243                 // It is OK to continue for now, even though the target root is not linked to the source root.
244                 // The question of whether to link the target root to the source root will asked at drop time.
245                 suggestions.add(DropSuggestions.linkToLibrary(graph, targetRoot, sourceRoot));
246             }
247
248             ModelingResources MOD = ModelingResources.getInstance(graph);
249             StructuralResource2 STR = StructuralResource2.getInstance(graph);
250
251             Resource configurationComposite = graph.getPossibleObject(dropTarget, MOD.DiagramToComposite);
252             Resource componentTypeFromDiagram = configurationComposite != null ? graph.getPossibleObject(configurationComposite, STR.Defines) : null;
253
254             // Prevent dragging to published components
255             if (componentTypeFromDiagram != null && Layer0Utils.isPublished(graph, componentTypeFromDiagram))
256                 return "Cannot create elements into a diagram that belongs to a published user component."; //$NON-NLS-1$
257
258             // Check if dragged object is symbol or component type and determine other
259             Resource componentType;
260             Resource symbol = graph.getPossibleObject(draggedResource, MOD.ComponentTypeToSymbol);
261             if (symbol != null)
262                 componentType = draggedResource;
263             else {
264                 componentType = graph.getPossibleObject(draggedResource, MOD.SymbolToComponentType);
265                 symbol = draggedResource;
266             }
267
268             // Prevent dragging a symbol of component type into its own configuration.
269             if (componentType != null
270                     && configurationComposite != null
271                     && componentTypeFromDiagram != null
272                     && componentType.equals(componentTypeFromDiagram)) {
273                 return "Cannot instantiate user component within its own configuration."; //$NON-NLS-1$
274             }
275
276             return symbol;
277         });
278     }
279
280     @Override
281     public void dragExit(DropTargetEvent dte, IDnDContext dp) {
282         // System.out.println("exit");
283     }
284
285     @Override
286     public void dragOver(DropTargetDragEvent dtde, IDnDContext dp) {
287         // System.out.println("over");
288     }
289
290     private IElement tryPick(Point p) {
291         Point2D canvas = transformUtil.controlToCanvas(p, null);
292
293         assertDependencies();
294
295         PickRequest     req                     = new PickRequest(canvas);
296         req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS;
297         List<IElement>  picks                   = new ArrayList<>();
298         pickContext.pick(diagram, req, picks);
299
300         if(picks.size() == 1) return picks.iterator().next();
301
302         return null;
303     }
304
305     @Override
306     public void drop(DropTargetDropEvent dtde, final IDnDContext dp) {
307         TimeLogger.resetTimeAndLog(getClass(), "drop"); //$NON-NLS-1$
308
309         final Point loc = dtde.getLocation();
310         final IDiagram d = diagram;
311         if (d == null)
312             return;
313
314         try {
315             validateDrop(d, dp, () -> performDrop(d, loc, dp));
316         } catch (DatabaseException e) {
317             LOGGER.error("Element drop validation failed", e); //$NON-NLS-1$
318         } 
319     }
320
321     private void validateDrop(IDiagram diagram, IDnDContext dp, Runnable dropFunction)
322             throws DatabaseException {
323         List<DropSuggestion> reqs = dp.getHints().getHint(KEY_SUGGESTIONS);
324         if (reqs != null && !reqs.isEmpty()) {
325             // Ask user if suggestions should be ran before dropping.
326             // If not, cancel.
327             Shell parentShell = partSite.getWorkbenchWindow().getShell();
328             SWTUtils.asyncExec(parentShell, () -> {
329                 if (parentShell.isDisposed())
330                     return;
331                 if (!DropSuggestions.askSuggestions(parentShell, reqs))
332                     return;
333
334                 try {
335                     Simantics.getSession().syncRequest(DropSuggestions.performSuggestionsRequest(reqs));
336
337                     getThread().asyncExec(() -> {
338                         if (isRemoved())
339                             return;
340                         dropFunction.run();
341                     });
342                 } catch (DatabaseException e) {
343                     String format = Messages.PopulateElementDropParticipant_PreDropFixesFailed;
344                     String formattedSuggestions = EString.implode(reqs);
345                     LOGGER.error(format, formattedSuggestions, e);
346                     ShowError.showError(Messages.PopulateElementDropParticipant_PreDropFixesFailed_Title, NLS.bind(format, formattedSuggestions), e);
347                 }
348             });
349         } else {
350             dropFunction.run();
351         }
352     }
353
354     private void performDrop(IDiagram d, Point loc, IDnDContext dp) {
355         IElement pick = tryPick(loc);
356         if (pick != null) {
357             List<WorkbenchSelectionElement> wses = Arrays.stream(dp.toArray())
358                     .filter(WSEDragItem.class::isInstance)
359                     .map(di -> ((WSEDragItem) di).getObject())
360                     .collect(Collectors.toList());
361
362             final Resource element = (Resource) ElementUtils.getData(d, pick);
363             if (element != null && !wses.isEmpty()) {
364                 try {
365                     Session db = Simantics.getSession();
366                     DiagramResource DIA = DiagramResource.getInstance(db);
367                     Variable function = db.syncRequest(new PossibleVariableProperty(element, DIA.symbolDropHandler));
368                     if (function != null) {
369                         db.syncRequest((Write) graph -> Simantics.invokeSCLWrite(graph, function, wses));
370                         return;
371                     }
372                 } catch (DatabaseException e) {
373                     Activator.getDefault().getLog()
374                             .log(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
375                                     "Invocation to custom symbolDropHandler for element " //$NON-NLS-1$
376                                             + element + " failed.", //$NON-NLS-1$
377                                     e));
378                     return;
379                 }
380             }
381         }
382
383         Runnable creator = () -> {
384             DiagramUtils.mutateDiagram(d, m -> {
385                 IDragItem items[] = dp.toArray();
386
387                 for (IDragItem i : items) {
388                     if (!(i instanceof ElementClassDragItem))
389                         continue;
390
391                     ElementClassDragItem res = (ElementClassDragItem) i;
392                     ElementClass ec = res.getElementClass();
393
394                     Point2D pos = dp.getItemPosition(i);
395                     // System.out.println(pos);
396                     assert (pos != null);
397
398                     IElement element = m.newElement(ec);
399                     element.setHints(res.getHintContext().getHints());
400
401                     setupDroppedElement(element, pos);
402
403                     // Remove only the drag items we've processed.
404                     dp.remove(i);
405                 }
406             });
407         };
408
409         selectNewDiagramContentAfter(d, partSite, creator);
410
411         getContext().getContentContext().setDirty();
412     }
413
414     private static class PossibleVariableProperty extends ResourceRead2<Variable> {
415
416         public PossibleVariableProperty(Resource entity, Resource property) {
417             super(entity, property);
418         }
419
420         @Override
421         public Variable perform(ReadGraph graph) throws DatabaseException {
422             return Variables.tryGetProperty(graph, resource, resource2);
423         }
424
425     }
426
427     protected void selectNewDiagramContentAfter(IDiagram d, IWorkbenchPartSite activateSite, Runnable diagramModifier) {
428         try {
429             Resource diagramResource = d.getHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE);
430             final DiagramContentTracker tracker = diagramResource == null ? null
431                     : DiagramContentTracker.start(getContext(), Simantics.getSession(), diagramResource);
432
433             diagramModifier.run();
434
435             if (tracker != null) {
436                 // Get difference of diagram contents to find out what was added.
437                 DiagramContentChanges changes = tracker.update();
438                 Set<Resource> addedElements = changes.pick(changes.elements, Change.ADDED);
439                 if (!addedElements.isEmpty()) {
440                     new DiagramSelectionUpdater(getContext())
441                     .setNewSelection(0, addedElements)
442                     .setOneshot(true)
443                     .track();
444                     if (activateSite != null)
445                         WorkbenchUtils.activatePart(activateSite);
446                 }
447             }
448         } catch (DatabaseException e) {
449             Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Diagram content change tracking failed.", e)); //$NON-NLS-1$
450         }
451     }
452
453     protected void setupDroppedElement(IElement element, Point2D dropPos) {
454         // This works only for elements without parents.
455         ISnapAdvisor snapAdvisor = getContext().getHintStack().getHint(DiagramHints.SNAP_ADVISOR);
456         if(snapAdvisor != null)
457             snapAdvisor.snap(dropPos);
458
459         IElement parent = element.getHint(ElementHints.KEY_PARENT_ELEMENT);
460         if (parent != null) {
461             Point2D parentPos = ElementUtils.getPos(parent);
462             Point2D pos = new Point2D.Double(dropPos.getX() - parentPos.getX(), dropPos.getY() - parentPos.getY());
463             ElementUtils.setPos(element, pos);
464         } else {
465             ElementUtils.setPos(element, dropPos);
466         }
467     }
468
469     @Override
470     public void dropActionChanged(DropTargetDragEvent dtde, IDnDContext dp) {
471         dtde.acceptDrag(DnDConstants.ACTION_COPY);
472     }
473
474     @Override
475     public int getAllowedOps() {
476         return DnDConstants.ACTION_COPY;
477     }
478
479     @Override
480     public double getPriority() {
481         return 10.0;
482     }
483
484 }