]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.diagram/src/org/simantics/diagram/symbollibrary/ui/SymbolLibraryComposite.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.diagram / src / org / simantics / diagram / symbollibrary / ui / SymbolLibraryComposite.java
1 /*******************************************************************************\r
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
3  * in Industry THTH ry.\r
4  * All rights reserved. This program and the accompanying materials\r
5  * are made available under the terms of the Eclipse Public License v1.0\r
6  * which accompanies this distribution, and is available at\r
7  * http://www.eclipse.org/legal/epl-v10.html\r
8  *\r
9  * Contributors:\r
10  *     VTT Technical Research Centre of Finland - initial API and implementation\r
11  *******************************************************************************/\r
12 package org.simantics.diagram.symbollibrary.ui;\r
13 \r
14 import java.awt.datatransfer.StringSelection;\r
15 import java.awt.datatransfer.Transferable;\r
16 import java.awt.dnd.DnDConstants;\r
17 import java.awt.dnd.DragGestureEvent;\r
18 import java.awt.dnd.DragSourceDragEvent;\r
19 import java.awt.dnd.DragSourceDropEvent;\r
20 import java.awt.dnd.DragSourceEvent;\r
21 import java.lang.ref.SoftReference;\r
22 import java.util.ArrayList;\r
23 import java.util.Collection;\r
24 import java.util.Collections;\r
25 import java.util.Comparator;\r
26 import java.util.EnumSet;\r
27 import java.util.HashMap;\r
28 import java.util.Iterator;\r
29 import java.util.List;\r
30 import java.util.Map;\r
31 import java.util.Set;\r
32 import java.util.TreeSet;\r
33 import java.util.WeakHashMap;\r
34 import java.util.concurrent.ExecutorService;\r
35 import java.util.concurrent.Semaphore;\r
36 import java.util.concurrent.SynchronousQueue;\r
37 import java.util.concurrent.ThreadFactory;\r
38 import java.util.concurrent.ThreadPoolExecutor;\r
39 import java.util.concurrent.TimeUnit;\r
40 import java.util.concurrent.atomic.AtomicBoolean;\r
41 import java.util.concurrent.atomic.AtomicInteger;\r
42 import java.util.regex.Matcher;\r
43 import java.util.regex.Pattern;\r
44 \r
45 import org.eclipse.core.runtime.IAdaptable;\r
46 import org.eclipse.core.runtime.IStatus;\r
47 import org.eclipse.core.runtime.Status;\r
48 import org.eclipse.jface.layout.GridDataFactory;\r
49 import org.eclipse.jface.layout.GridLayoutFactory;\r
50 import org.eclipse.jface.resource.FontDescriptor;\r
51 import org.eclipse.jface.resource.JFaceResources;\r
52 import org.eclipse.jface.resource.LocalResourceManager;\r
53 import org.eclipse.jface.viewers.AcceptAllFilter;\r
54 import org.eclipse.jface.viewers.BaseLabelProvider;\r
55 import org.eclipse.jface.viewers.IFilter;\r
56 import org.eclipse.jface.viewers.ISelection;\r
57 import org.eclipse.jface.viewers.IStructuredContentProvider;\r
58 import org.eclipse.jface.viewers.StructuredSelection;\r
59 import org.eclipse.jface.viewers.Viewer;\r
60 import org.eclipse.jface.viewers.ViewerFilter;\r
61 import org.eclipse.nebula.widgets.pgroup.PGroup;\r
62 import org.eclipse.swt.SWT;\r
63 import org.eclipse.swt.custom.ScrolledComposite;\r
64 import org.eclipse.swt.events.ControlAdapter;\r
65 import org.eclipse.swt.events.ControlEvent;\r
66 import org.eclipse.swt.events.DisposeEvent;\r
67 import org.eclipse.swt.events.DisposeListener;\r
68 import org.eclipse.swt.events.ExpandEvent;\r
69 import org.eclipse.swt.events.ExpandListener;\r
70 import org.eclipse.swt.events.ModifyEvent;\r
71 import org.eclipse.swt.events.ModifyListener;\r
72 import org.eclipse.swt.graphics.Color;\r
73 import org.eclipse.swt.graphics.Point;\r
74 import org.eclipse.swt.graphics.Rectangle;\r
75 import org.eclipse.swt.layout.GridData;\r
76 import org.eclipse.swt.widgets.Composite;\r
77 import org.eclipse.swt.widgets.Control;\r
78 import org.eclipse.swt.widgets.Event;\r
79 import org.eclipse.swt.widgets.Listener;\r
80 import org.eclipse.swt.widgets.Widget;\r
81 import org.simantics.db.ReadGraph;\r
82 import org.simantics.db.Resource;\r
83 import org.simantics.db.common.procedure.adapter.ListenerAdapter;\r
84 import org.simantics.db.common.request.UnaryRead;\r
85 import org.simantics.db.exception.DatabaseException;\r
86 import org.simantics.diagram.internal.Activator;\r
87 import org.simantics.diagram.symbolcontribution.CompositeSymbolGroup;\r
88 import org.simantics.diagram.symbolcontribution.IIdentifiedObject;\r
89 import org.simantics.diagram.symbolcontribution.ISymbolProvider;\r
90 import org.simantics.diagram.symbolcontribution.IdentifiedObject;\r
91 import org.simantics.diagram.symbolcontribution.SymbolProviderFactory;\r
92 import org.simantics.diagram.symbollibrary.IModifiableSymbolGroup;\r
93 import org.simantics.diagram.symbollibrary.ISymbolGroup;\r
94 import org.simantics.diagram.symbollibrary.ISymbolGroupListener;\r
95 import org.simantics.diagram.symbollibrary.ISymbolItem;\r
96 import org.simantics.diagram.symbollibrary.ui.FilterConfiguration.Mode;\r
97 import org.simantics.diagram.synchronization.ErrorHandler;\r
98 import org.simantics.diagram.synchronization.LogErrorHandler;\r
99 import org.simantics.diagram.synchronization.SynchronizationHints;\r
100 import org.simantics.g2d.canvas.Hints;\r
101 import org.simantics.g2d.canvas.ICanvasContext;\r
102 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;\r
103 import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;\r
104 import org.simantics.g2d.chassis.AWTChassis;\r
105 import org.simantics.g2d.diagram.DiagramUtils;\r
106 import org.simantics.g2d.diagram.handler.PickContext;\r
107 import org.simantics.g2d.diagram.handler.PickRequest;\r
108 import org.simantics.g2d.diagram.handler.layout.FlowLayout;\r
109 import org.simantics.g2d.diagram.participant.AbstractDiagramParticipant;\r
110 import org.simantics.g2d.diagram.participant.Selection;\r
111 import org.simantics.g2d.diagram.participant.pointertool.PointerInteractor;\r
112 import org.simantics.g2d.dnd.IDragSourceParticipant;\r
113 import org.simantics.g2d.element.ElementClass;\r
114 import org.simantics.g2d.element.ElementHints;\r
115 import org.simantics.g2d.element.IElement;\r
116 import org.simantics.g2d.element.handler.StaticSymbol;\r
117 import org.simantics.g2d.event.adapter.SWTMouseEventAdapter;\r
118 import org.simantics.g2d.gallery.GalleryViewer;\r
119 import org.simantics.g2d.gallery.ILabelProvider;\r
120 import org.simantics.g2d.image.DefaultImages;\r
121 import org.simantics.g2d.image.Image;\r
122 import org.simantics.g2d.image.Image.Feature;\r
123 import org.simantics.g2d.image.impl.ImageProxy;\r
124 import org.simantics.g2d.participant.TransformUtil;\r
125 import org.simantics.scenegraph.g2d.events.EventTypes;\r
126 import org.simantics.scenegraph.g2d.events.IEventHandler;\r
127 import org.simantics.scenegraph.g2d.events.MouseEvent;\r
128 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent;\r
129 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;\r
130 import org.simantics.scl.runtime.tuple.Tuple2;\r
131 import org.simantics.ui.SimanticsUI;\r
132 import org.simantics.ui.dnd.LocalObjectTransfer;\r
133 import org.simantics.ui.dnd.LocalObjectTransferable;\r
134 import org.simantics.ui.dnd.MultiTransferable;\r
135 import org.simantics.ui.dnd.PlaintextTransfer;\r
136 import org.simantics.utils.datastructures.cache.ProvisionException;\r
137 import org.simantics.utils.datastructures.hints.IHintContext;\r
138 import org.simantics.utils.threads.AWTThread;\r
139 import org.simantics.utils.threads.IThreadWorkQueue;\r
140 import org.simantics.utils.threads.SWTThread;\r
141 import org.simantics.utils.threads.ThreadUtils;\r
142 import org.simantics.utils.ui.ErrorLogger;\r
143 import org.simantics.utils.ui.ExceptionUtils;\r
144 \r
145 /**\r
146  * @author Tuukka Lehtonen\r
147  */\r
148 public class SymbolLibraryComposite extends Composite {\r
149 \r
150     private static final int    FILTER_DELAY           = 500;\r
151 \r
152     private static final String KEY_VIEWER_INITIALIZED = "viewer.initialized";\r
153     private static final String KEY_USER_EXPANDED      = "userExpanded";\r
154     private static final String KEY_GROUP_FILTERED     = "groupFiltered";\r
155 \r
156     /** Root composite */\r
157     ScrolledComposite           sc;\r
158     Composite                   c;\r
159     IThreadWorkQueue            swtThread;\r
160     boolean                     defaultExpanded = false;\r
161     ISymbolProvider             symbolProvider;\r
162     AtomicBoolean               disposed = new AtomicBoolean(false);\r
163 \r
164     /**\r
165      * This value is incremented each time a load method is called and symbol\r
166      * group population is started. It can be used by\r
167      * {@link #populateGroups(ExecutorService, Control, Iterator, IFilter)} to\r
168      * tell whether it should stop its population job because a later load\r
169      * will override its results anyway.\r
170      */\r
171     AtomicInteger                               loadCount              = new AtomicInteger();\r
172 \r
173     Map<ISymbolGroup, PGroup>                   groups                 = new HashMap<>();\r
174     Map<ISymbolGroup, GalleryViewer>            groupViewers           = new HashMap<>();\r
175     Map<Object, Boolean>                        expandedGroups         = new HashMap<>();\r
176     LocalResourceManager                        resourceManager;\r
177     FilterArea                                  filter;\r
178 \r
179     ThreadFactory threadFactory = new ThreadFactory() {\r
180         @Override\r
181         public Thread newThread(Runnable r) {\r
182             Thread t = new Thread(r, "Symbol Library Loader");\r
183             t.setDaemon(false);\r
184             t.setPriority(Thread.NORM_PRIORITY);\r
185             return t;\r
186         }\r
187     };\r
188 \r
189     Semaphore                                   loaderSemaphore        = new Semaphore(1);\r
190     ExecutorService                             loaderExecutor         = new ThreadPoolExecutor(0, Integer.MAX_VALUE,\r
191             2L, TimeUnit.SECONDS,\r
192             new SynchronousQueue<Runnable>(),\r
193             threadFactory);\r
194 \r
195     /**\r
196      * Used to prevent annoying reloading of symbols when groups are closed and\r
197      * reopened by not always having to schedule an {@link ImageLoader} in\r
198      * {@link LabelProvider#getImage(Object)}.\r
199      */\r
200     Map<ISymbolItem, SoftReference<ImageProxy>> imageCache = new WeakHashMap<ISymbolItem, SoftReference<ImageProxy>>();\r
201 \r
202     static final Pattern                        ANY                    = Pattern.compile(".*");\r
203     Pattern                                     currentFilterPattern   = ANY;\r
204 \r
205     FilterConfiguration                         config                 = new FilterConfiguration();\r
206     IFilter                                     currentGroupFilter     = AcceptAllFilter.getInstance();\r
207 \r
208     ErrorHandler                                errorHandler           = LogErrorHandler.INSTANCE;\r
209 \r
210     static class GroupDescriptor {\r
211         public final ISymbolGroup lib;\r
212         public final String       label;\r
213         public final String       description;\r
214         public final PGroup       group;\r
215 \r
216         public GroupDescriptor(ISymbolGroup lib, String label, String description, PGroup group) {\r
217             assert(lib != null);\r
218             assert(label != null);\r
219             this.lib = lib;\r
220             this.label = label;\r
221             this.description = description;\r
222             this.group = group;\r
223         }\r
224     }\r
225 \r
226     Comparator<GroupDescriptor> groupComparator = new Comparator<GroupDescriptor>() {\r
227         @Override\r
228         public int compare(GroupDescriptor o1, GroupDescriptor o2) {\r
229             return o1.label.compareToIgnoreCase(o2.label);\r
230         }\r
231     };\r
232 \r
233     static final EnumSet<Feature> VOLATILE = EnumSet.of(Feature.Volatile);\r
234 \r
235     static class PendingImage extends ImageProxy {\r
236         EnumSet<Feature> features;\r
237         PendingImage(Image source, EnumSet<Feature> features) {\r
238             super(source);\r
239             this.features = features;\r
240         }\r
241         @Override\r
242         public EnumSet<Feature> getFeatures() {\r
243             return features;\r
244         }\r
245     }\r
246 \r
247     class LabelProvider extends BaseLabelProvider implements ILabelProvider {\r
248         @Override\r
249         public Image getImage(final Object element) {\r
250             ISymbolItem item = (ISymbolItem) element;\r
251             // Use a volatile ImageProxy to make the image loading asynchronous.\r
252             ImageProxy proxy = null;\r
253             SoftReference<ImageProxy> proxyRef = imageCache.get(item);\r
254             if (proxyRef != null)\r
255                 proxy = proxyRef.get();\r
256             if (proxy == null) {\r
257                 proxy = new PendingImage(DefaultImages.HOURGLASS.get(), VOLATILE);\r
258                 imageCache.put(item, new SoftReference<ImageProxy>(proxy));\r
259                 ThreadUtils.getNonBlockingWorkExecutor().schedule(new ImageLoader(proxy, item), 100, TimeUnit.MILLISECONDS);\r
260             }\r
261             return proxy;\r
262         }\r
263         @Override\r
264         public String getText(final Object element) {\r
265             return ((ISymbolItem) element).getName();\r
266         }\r
267         @Override\r
268         public String getToolTipText(Object element) {\r
269             ISymbolItem item = (ISymbolItem) element;\r
270             String name = item.getName();\r
271             String desc = item.getDescription();\r
272             return name.equals(desc) ? name : name + " - " + desc;\r
273         }\r
274 \r
275         @Override\r
276         public java.awt.Image getToolTipImage(Object object) {\r
277             return null;\r
278         }\r
279         @Override\r
280         public Color getToolTipBackgroundColor(Object object) {\r
281             return null;\r
282         }\r
283 \r
284         @Override\r
285         public Color getToolTipForegroundColor(Object object) {\r
286             return null;\r
287         }\r
288     }\r
289 \r
290     public SymbolLibraryComposite(final Composite parent, int style, SymbolProviderFactory symbolProvider) {\r
291         super(parent, style);\r
292         init(parent, style);\r
293         SimanticsUI.getSession().asyncRequest(new CreateSymbolProvider(symbolProvider), new SymbolProviderListener());\r
294         addDisposeListener(new DisposeListener() {\r
295             @Override\r
296             public void widgetDisposed(DisposeEvent e) {\r
297                 disposed.set(true);\r
298             }\r
299         });\r
300     }\r
301 \r
302     /**\r
303      *\r
304      */\r
305     static class CreateSymbolProvider extends UnaryRead<SymbolProviderFactory, ISymbolProvider> {\r
306         public CreateSymbolProvider(SymbolProviderFactory factory) {\r
307             super(factory);\r
308         }\r
309         @Override\r
310         public ISymbolProvider perform(ReadGraph graph) throws DatabaseException {\r
311             //System.out.println("CreateSymbolProvider.perform: " + parameter);\r
312             ISymbolProvider provider = parameter.create(graph);\r
313             //print(provider);\r
314             return provider;\r
315         }\r
316     }\r
317 \r
318     @SuppressWarnings("unused")\r
319     private static void print(ISymbolProvider provider) {\r
320         for (ISymbolGroup grp : provider.getSymbolGroups()) {\r
321             System.out.println("GROUP: " + grp);\r
322             if (grp instanceof CompositeSymbolGroup) {\r
323                 CompositeSymbolGroup cgrp = (CompositeSymbolGroup) grp;\r
324                 for (ISymbolGroup grp2 : cgrp.getGroups()) {\r
325                     System.out.println("\tGROUP: " + grp2);\r
326                 }\r
327             }\r
328         }\r
329     }\r
330 \r
331     /**\r
332      *\r
333      */\r
334     class SymbolProviderListener extends ListenerAdapter<ISymbolProvider> {\r
335         @Override\r
336         public void exception(Throwable t) {\r
337             ErrorLogger.defaultLogError(t);\r
338         }\r
339         @Override\r
340         public void execute(ISymbolProvider result) {\r
341             //System.out.println("SymbolProviderListener: " + result);\r
342             symbolProvider = result;\r
343             if (result != null) {\r
344                 Collection<ISymbolGroup> groups = result.getSymbolGroups();\r
345                 //print(result);\r
346                 load(groups);\r
347             }\r
348         }\r
349         public boolean isDisposed() {\r
350                 boolean result = SymbolLibraryComposite.this.isDisposed(); \r
351             return result;\r
352         }\r
353     }\r
354 \r
355     private void init(final Composite parent, int style) {\r
356         GridLayoutFactory.fillDefaults().spacing(0,0).applyTo(this);\r
357 //        setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_RED));\r
358 \r
359         this.resourceManager = new LocalResourceManager(JFaceResources.getResources(getDisplay()), this);\r
360         swtThread = SWTThread.getThreadAccess(this);\r
361 \r
362         filter = new FilterArea(this, SWT.NONE);\r
363         GridDataFactory.fillDefaults().grab(true, false).applyTo(filter);\r
364         filter.getText().addModifyListener(new ModifyListener() {\r
365             int modCount = 0;\r
366             //long lastModificationTime = -1000;\r
367             @Override\r
368             public void modifyText(ModifyEvent e) {\r
369                 scheduleDelayedFilter(FILTER_DELAY, TimeUnit.MILLISECONDS);\r
370             }\r
371             private void scheduleDelayedFilter(long filterDelay, TimeUnit delayUnit) {\r
372                 final String text = filter.getText().getText();\r
373 \r
374                 //long time = System.currentTimeMillis();\r
375                 //long delta = time - lastModificationTime;\r
376                 //lastModificationTime = time;\r
377 \r
378                 final int count = ++modCount;\r
379                 ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {\r
380                     @Override\r
381                     public void run() {\r
382                         int newCount = modCount;\r
383                         if (newCount != count)\r
384                             return;\r
385 \r
386                         ThreadUtils.asyncExec(swtThread, new Runnable() {\r
387                             @Override\r
388                             public void run() {\r
389                                 if (sc.isDisposed())\r
390                                     return;\r
391                                 if (!filterGroups(text)) {\r
392                                     scheduleDelayedFilter(100, TimeUnit.MILLISECONDS);\r
393                                 }\r
394                             }\r
395                         });\r
396                     }\r
397                 }, filterDelay, delayUnit);\r
398             }\r
399         });\r
400 \r
401         sc = new ScrolledComposite(this, SWT.V_SCROLL);\r
402         GridDataFactory.fillDefaults().grab(true, true).applyTo(sc);\r
403         sc.setAlwaysShowScrollBars(false);\r
404         sc.setExpandHorizontal(false);\r
405         sc.setExpandVertical(false);\r
406         sc.getVerticalBar().setIncrement(30);\r
407         sc.getVerticalBar().setPageIncrement(200);\r
408         sc.addControlListener( new ControlAdapter() {\r
409             @Override\r
410             public void controlResized(ControlEvent e) {\r
411                 //System.out.println("ScrolledComposite resized: " + sc.getSize());\r
412                 refreshScrolledComposite();\r
413             }\r
414         });\r
415         //sc.setBackground(sc.getDisplay().getSystemColor(SWT.COLOR_RED));\r
416 \r
417         c = new Composite(sc, 0);\r
418         c.setVisible(false);\r
419         GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(c);\r
420         //c.setBackground(c.getDisplay().getSystemColor(SWT.COLOR_BLUE));\r
421 \r
422         sc.setContent(c);\r
423 \r
424         // No event context <-> mouse on empty space in symbol library\r
425         SWTMouseEventAdapter noContextEventAdapter = new SWTMouseEventAdapter(null, externalEventHandler);\r
426         installMouseEventAdapter(sc, noContextEventAdapter);\r
427         installMouseEventAdapter(c, noContextEventAdapter);\r
428 \r
429         c.addDisposeListener(new DisposeListener() {\r
430             @Override\r
431             public void widgetDisposed(DisposeEvent e) {\r
432                 // Remember to shutdown the executor\r
433                 loaderExecutor.shutdown();\r
434                 groupViewers.clear();\r
435             }\r
436         });\r
437     }\r
438 \r
439     void refreshScrolledComposite() {\r
440         // Execute asynchronously to give the UI events triggering this method\r
441         // call time to run through before actually doing any resizing.\r
442         // Otherwise the result will lag behind reality when scrollbar\r
443         // visibility is toggled by the toolkit.\r
444         ThreadUtils.asyncExec(swtThread, new Runnable() {\r
445             @Override\r
446             public void run() {\r
447                 if (sc.isDisposed())\r
448                     return;\r
449                 syncRefreshScrolledComposite();\r
450             }\r
451         });\r
452     }\r
453 \r
454     void syncRefreshScrolledComposite() {\r
455         // Execute asynchronously to give the UI events triggering this method\r
456         // call time to run through before actually doing any resizing.\r
457         // Otherwise the result will lag behind reality when scrollbar\r
458         // visibility is toggled by the toolkit.\r
459         Rectangle r = sc.getClientArea();\r
460         Point contentSize = c.computeSize(r.width, SWT.DEFAULT);\r
461         //System.out.println("[" + Thread.currentThread() + "] computed content size: " + contentSize + ", " + r);\r
462         c.setSize(contentSize);\r
463     }\r
464 \r
465     /**\r
466      * (Re-)Load symbol groups, refresh the content\r
467      */\r
468     void load(Collection<ISymbolGroup> _libraries) {\r
469         if (_libraries == null)\r
470             _libraries = Collections.emptyList();\r
471         final Collection<ISymbolGroup> libraries = _libraries;\r
472         if (loaderExecutor.isShutdown())\r
473             return;\r
474         loaderExecutor.execute(new Runnable() {\r
475             @Override\r
476             public void run() {\r
477                 // Increment loadCount to signal that a new load cycle is on the way.\r
478                 Integer loadId = loadCount.incrementAndGet();\r
479                 try {\r
480                     loaderSemaphore.acquire();\r
481                     beginPopulate(loadId);\r
482                 } catch (InterruptedException e) {\r
483                     ExceptionUtils.logError(e);\r
484                 } catch (RuntimeException e) {\r
485                     loaderSemaphore.release();\r
486                     ExceptionUtils.logAndShowError(e);\r
487                 } catch (Error e) {\r
488                     loaderSemaphore.release();\r
489                     ExceptionUtils.logAndShowError(e);\r
490                 }\r
491             }\r
492 \r
493             void beginPopulate(Integer loadId) {\r
494                 synchronized (groups) {\r
495                     // Must use toArray since groups are removed within the loop\r
496                     for (Iterator<Map.Entry<ISymbolGroup, PGroup>> it = groups.entrySet().iterator(); it.hasNext();) {\r
497                         Map.Entry<ISymbolGroup, PGroup> entry = it.next();\r
498                         if (!libraries.contains(entry.getKey())) {\r
499                             PGroup group = entry.getValue();\r
500                             it.remove();\r
501                             groupViewers.remove(entry.getKey());\r
502                             if (group != null && !group.isDisposed())\r
503                                 ThreadUtils.asyncExec(swtThread, disposer(group));\r
504                         }\r
505                     }\r
506                     Set<GroupDescriptor> groupDescs = new TreeSet<GroupDescriptor>(groupComparator);\r
507                     for (ISymbolGroup lib : libraries) {\r
508                         PGroup group = groups.get(lib);\r
509                         //String label = group != null ? group.getText() : lib.getName();\r
510                         String label = lib.getName();\r
511                         String description = lib.getDescription();\r
512                         groupDescs.add(new GroupDescriptor(lib, label, description, group));\r
513                     }\r
514 \r
515                     // Populate all the missing groups.\r
516                     IFilter groupFilter = currentGroupFilter;\r
517                     populateGroups(\r
518                             loaderExecutor,\r
519                             null,\r
520                             groupDescs.iterator(),\r
521                             groupFilter,\r
522                             loadId,\r
523                             new Runnable() {\r
524                                 @Override\r
525                                 public void run() {\r
526                                     loaderSemaphore.release();\r
527                                 }\r
528                             });\r
529                 }\r
530             }\r
531         });\r
532     }\r
533 \r
534     void populateGroups(\r
535             final ExecutorService exec,\r
536             final Control lastGroup,\r
537             final Iterator<GroupDescriptor> iter,\r
538             final IFilter groupFilter,\r
539             final Integer loadId,\r
540             final Runnable loadComplete)\r
541     {\r
542         // Check whether to still continue this population or not.\r
543         int currentLoadId = loadCount.get();\r
544         if (currentLoadId != loadId) {\r
545             loadComplete.run();\r
546             return;\r
547         }\r
548 \r
549         if (!iter.hasNext()) {\r
550             ThreadUtils.asyncExec(swtThread, new Runnable() {\r
551                 @Override\r
552                 public void run() {\r
553                     if (filter.isDisposed() || c.isDisposed())\r
554                         return;\r
555                     //filter.focus();\r
556                     c.setVisible(true);\r
557                 }\r
558             });\r
559             loadComplete.run();\r
560             return;\r
561         }\r
562 \r
563         final GroupDescriptor desc = iter.next();\r
564 \r
565         ThreadUtils.asyncExec(swtThread, new Runnable() {\r
566             @Override\r
567             public void run() {\r
568                 // Must make sure that loadComplete is invoked under error\r
569                 // circumstances.\r
570                 try {\r
571                     populateGroup();\r
572                 } catch (RuntimeException e) {\r
573                     loadComplete.run();\r
574                     ExceptionUtils.logAndShowError(e);\r
575                 } catch (Error e) {\r
576                     loadComplete.run();\r
577                     ExceptionUtils.logAndShowError(e);\r
578                 }\r
579             }\r
580 \r
581             public void populateGroup() {\r
582                 if (c.isDisposed()) {\r
583                     loadComplete.run();\r
584                     return;\r
585                 }\r
586                 // $ SWT-begin\r
587                 //System.out.println("populating: " + desc.label);\r
588                 PGroup group = desc.group;\r
589                 Runnable chainedCompletionCallback = loadComplete;\r
590                 if (group == null || group.isDisposed()) {\r
591 \r
592                     group = new PGroup(c, SWT.NONE);\r
593 //                    group.addListener(SWT.KeyUp, filterActivationListener);\r
594 //                    group.addListener(SWT.KeyDown, filterActivationListener);\r
595 //                    group.addListener(SWT.FocusIn, filterActivationListener);\r
596 //                    group.addListener(SWT.FocusOut, filterActivationListener);\r
597 //                    group.addListener(SWT.MouseDown, filterActivationListener);\r
598 //                    group.addListener(SWT.MouseUp, filterActivationListener);\r
599 //                    group.addListener(SWT.MouseDoubleClick, filterActivationListener);\r
600 //                    group.addListener(SWT.Arm, filterActivationListener);\r
601                     if (lastGroup != null) {\r
602                         group.moveBelow(lastGroup);\r
603                     } else {\r
604                         group.moveAbove(null);\r
605                     }\r
606 \r
607                     installMouseEventAdapter(group, new SWTMouseEventAdapter(group, externalEventHandler));\r
608 \r
609                     groups.put(desc.lib, group);\r
610 \r
611                     Boolean shouldBeExpanded = expandedGroups.get(symbolGroupToKey(desc.lib));\r
612                     if (shouldBeExpanded == null)\r
613                         shouldBeExpanded = defaultExpanded;\r
614                     group.setData(KEY_USER_EXPANDED, shouldBeExpanded);\r
615 \r
616                     group.setExpanded(shouldBeExpanded);\r
617                     group.setFont(resourceManager.createFont(FontDescriptor.createFrom(group.getFont()).setStyle(SWT.NORMAL).increaseHeight(-1)));\r
618                     GridDataFactory.fillDefaults().align(SWT.FILL, SWT.BEGINNING).grab(true, false).applyTo(group);\r
619                     GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(group);\r
620                     group.addExpandListener(groupExpandListener);\r
621 \r
622                     // Track group content changes if possible.\r
623                     if (desc.lib instanceof IModifiableSymbolGroup) {\r
624                         IModifiableSymbolGroup mod = (IModifiableSymbolGroup) desc.lib;\r
625                         mod.addListener(groupListener);\r
626                     }\r
627 \r
628                     if (shouldBeExpanded) {\r
629                         //System.out.println("WAS EXPANDED(" + desc.label + ", " + symbolGroupToKey(desc.lib) + ", " + shouldBeExpanded + ")");\r
630                         PGroup expandedGroup = group;\r
631                         chainedCompletionCallback = () -> {\r
632                             // Chain callback to expand this group when the loading is otherwise completed.\r
633                             ThreadUtils.asyncExec(swtThread, () -> setExpandedState(expandedGroup, true, true));\r
634                             loadComplete.run();\r
635                         };\r
636                     }\r
637                 }\r
638 \r
639                 group.setData(SymbolLibraryKeys.KEY_GROUP, desc.lib);\r
640                 group.setText(desc.label);\r
641                 group.setToolTipText(desc.description);\r
642 \r
643                 // Hide the group immediately if necessary.\r
644                 boolean groupFiltered = !groupFilter.select(desc.label);\r
645                 group.setData(KEY_GROUP_FILTERED, Boolean.valueOf(groupFiltered));\r
646                 if (groupFiltered)\r
647                     setGroupVisible(group, false);\r
648 \r
649                 syncRefreshScrolledComposite();\r
650 \r
651                 final PGroup group_ = group;\r
652                 Runnable newCompletionCallback = chainedCompletionCallback;\r
653                 exec.execute(() -> {\r
654                     populateGroups(exec, group_, iter, groupFilter, loadId, newCompletionCallback);\r
655                 });\r
656             }\r
657         });\r
658     }\r
659 \r
660     protected void installMouseEventAdapter(Control onControl, SWTMouseEventAdapter eventAdapter) {\r
661         onControl.addMouseListener(eventAdapter);\r
662         onControl.addMouseTrackListener(eventAdapter);\r
663         onControl.addMouseMoveListener(eventAdapter);\r
664         onControl.addMouseWheelListener(eventAdapter);\r
665     }\r
666 \r
667     /**\r
668      * @param group\r
669      * @return <code>null</code> if GalleryViewer is currently being created\r
670      */\r
671     GalleryViewer initializeGroup(final PGroup group) {\r
672         if (group.isDisposed())\r
673             return null;\r
674 \r
675         //System.out.println("initializeGroup(" + group.getText() + ")");\r
676 \r
677         synchronized (group) {\r
678             if (group.getData(KEY_VIEWER_INITIALIZED) != null) {\r
679                 return (GalleryViewer) group.getData(SymbolLibraryKeys.KEY_GALLERY);\r
680             }\r
681             group.setData(KEY_VIEWER_INITIALIZED, Boolean.TRUE);\r
682         }\r
683 \r
684         //System.out.println("initializing group: " + group.getText());\r
685 \r
686         // NOTE: this will NOT stop to wait until the SWT/AWT UI\r
687         // population has been completed.\r
688         GalleryViewer viewer = new GalleryViewer(group);\r
689 \r
690         ISymbolGroup input = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP);\r
691         initializeViewer(group, input, viewer);\r
692 \r
693         groupViewers.put(input, viewer);\r
694         group.setData(SymbolLibraryKeys.KEY_GALLERY, viewer);\r
695 \r
696         //System.out.println("initialized group: " + group.getText());\r
697 \r
698         return viewer;\r
699     }\r
700 \r
701     void initializeViewer(final PGroup group, final ISymbolGroup input, final GalleryViewer viewer) {\r
702         GridDataFactory.fillDefaults().align(SWT.FILL, SWT.BEGINNING).grab(true, false).applyTo(viewer.getControl());\r
703         viewer.addDragSupport(new DragSourceParticipant());\r
704         viewer.setAlign(FlowLayout.Align.Left);\r
705         viewer.getDiagram().setHint(SynchronizationHints.ERROR_HANDLER, errorHandler);\r
706 \r
707         viewer.setContentProvider(new IStructuredContentProvider() {\r
708 \r
709             /**\r
710              * Returns the elements in the input, which must be either an array or a\r
711              * <code>Collection</code>.\r
712              */\r
713             @Override\r
714             public Object[] getElements(Object inputElement) {\r
715                 if(inputElement == null) return new Object[0];\r
716                 return ((ISymbolGroup)inputElement).getItems();\r
717             }\r
718 \r
719             /**\r
720              * This implementation does nothing.\r
721              */\r
722             @Override\r
723             public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {\r
724                 // do nothing.\r
725             }\r
726 \r
727             /**\r
728              * This implementation does nothing.\r
729              */\r
730             @Override\r
731             public void dispose() {\r
732                 // do nothing.\r
733             }\r
734 \r
735         });\r
736         viewer.setLabelProvider(new LabelProvider());\r
737         viewer.setInput(input);\r
738 \r
739         // Add event handler that closes libraries on double clicks into empty\r
740         // space in library.\r
741         viewer.getCanvasContext().getEventHandlerStack().add(new IEventHandler() {\r
742             @Override\r
743             public int getEventMask() {\r
744                 return EventTypes.MouseDoubleClickMask;\r
745             }\r
746 \r
747             @Override\r
748             public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) {\r
749                 if (externalEventHandler.handleEvent(e))\r
750                     return true;\r
751 \r
752                 if (e instanceof MouseDoubleClickedEvent) {\r
753                     PickRequest req = new PickRequest(((MouseDoubleClickedEvent) e).controlPosition);\r
754                     Collection<IElement> result = new ArrayList<IElement>();\r
755                     DiagramUtils.pick(viewer.getDiagram(), req, result);\r
756                     if (!result.isEmpty())\r
757                         return false;\r
758 \r
759                     //System.out.println("NOTHING CLICKED");\r
760                     if (group.isDisposed())\r
761                         return false;\r
762                     group.getDisplay().asyncExec(() -> {\r
763                         if (group.isDisposed())\r
764                             return;\r
765 \r
766                         boolean exp = !group.getExpanded();\r
767                         group.setData(KEY_USER_EXPANDED, Boolean.valueOf(exp));\r
768                         setGroupExpandedWithoutNotification(group, exp);\r
769                         refreshScrolledComposite();\r
770                     });\r
771                     return true;\r
772                 }\r
773                 return false;\r
774             }\r
775         }, 0);\r
776     }\r
777 \r
778     static String toPatternString(String filter) {\r
779         return DefaultFilterStrategy.defaultToPatternString(filter, true);\r
780     }\r
781 \r
782     static class SymbolItemFilter extends ViewerFilter {\r
783         private final String string;\r
784         private final Matcher m;\r
785 \r
786         public SymbolItemFilter(String string, Pattern pattern) {\r
787             this.string = string;\r
788             this.m = pattern.matcher("");\r
789         }\r
790 \r
791         @Override\r
792         public boolean select(Viewer viewer, Object parentElement, Object element) {\r
793             if (element instanceof ISymbolItem) {\r
794                 ISymbolItem item = (ISymbolItem) element;\r
795                 return matchesFilter(item.getName()) || matchesFilter(item.getDescription());\r
796             } else if (element instanceof ISymbolGroup) {\r
797                 ISymbolGroup group = (ISymbolGroup) element;\r
798                 return matchesFilter(group.getName());\r
799             }\r
800             return false;\r
801         }\r
802 \r
803         private boolean matchesFilter(String str) {\r
804             m.reset(str.toLowerCase());\r
805             boolean matches = m.matches();\r
806             //System.out.println(pattern + ": " + str + ": " + (matches ? "PASS" : "FAIL"));\r
807             return matches;\r
808         }\r
809 \r
810         @Override\r
811         public int hashCode() {\r
812             return string == null ? 0 : string.hashCode();\r
813         }\r
814 \r
815         @Override\r
816         public boolean equals(Object obj) {\r
817             if (this == obj)\r
818                 return true;\r
819             if (obj == null)\r
820                 return false;\r
821             if (getClass() != obj.getClass())\r
822                 return false;\r
823             SymbolItemFilter other = (SymbolItemFilter) obj;\r
824             if (string == null) {\r
825                 if (other.string != null)\r
826                     return false;\r
827             } else if (!string.equals(other.string))\r
828                 return false;\r
829             return true;\r
830         }\r
831     }\r
832 \r
833     static Pattern toPattern(String filterText) {\r
834         String regExFilter = toPatternString(filterText);\r
835         Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY;\r
836         return pattern;\r
837     }\r
838 \r
839     static IFilter composeFilter(final FilterConfiguration config) {\r
840         final Mode mode = config.getMode();\r
841         final List<Pattern> patterns = new ArrayList<Pattern>();\r
842         for (GroupFilter f : config.getFilters()) {\r
843             if (f.isActive())\r
844                 patterns.add(toPattern(f.getFilterText()));\r
845         }\r
846         return new IFilter() {\r
847             @Override\r
848             public boolean select(Object toTest) {\r
849                 if (patterns.isEmpty())\r
850                     return true;\r
851 \r
852                 String s = (String) toTest;\r
853                 switch (mode) {\r
854                     case AND:\r
855                         for (Pattern pat : patterns) {\r
856                             Matcher m = pat.matcher(s.toLowerCase());\r
857                             //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL"));\r
858                             if (!m.matches())\r
859                                 return false;\r
860                         }\r
861                         return true;\r
862                     case OR:\r
863                         for (Pattern pat : patterns) {\r
864                             Matcher m = pat.matcher(s.toLowerCase());\r
865                             //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL"));\r
866                             if (m.matches())\r
867                                 return true;\r
868                         }\r
869                         return false;\r
870                     default:\r
871                         throw new Error("Shouldn't happen");\r
872                 }\r
873             }\r
874         };\r
875     }\r
876 \r
877     void updateFilterConfiguration(FilterConfiguration config) {\r
878         this.config = config;\r
879         IFilter filter = composeFilter(config);\r
880         this.currentGroupFilter = filter;\r
881     }\r
882 \r
883     void applyGroupFilters() {\r
884         IFilter groupFilter = this.currentGroupFilter;\r
885         final boolean[] changed = new boolean[] { false };\r
886 \r
887         Control[] grps = c.getChildren();\r
888         for (Control ctrl : grps) {\r
889             final PGroup grp = (PGroup) ctrl;\r
890             boolean visible = grp.getVisible();\r
891             boolean shouldBeVisible = groupFilter.select(grp.getText());\r
892             boolean change = visible != shouldBeVisible;\r
893             changed[0] |= change;\r
894 \r
895             grp.setData(KEY_GROUP_FILTERED, Boolean.valueOf(!shouldBeVisible));\r
896             if (change) {\r
897                 setGroupVisible(grp, shouldBeVisible);\r
898             }\r
899         }\r
900 \r
901         ThreadUtils.asyncExec(swtThread, new Runnable() {\r
902             @Override\r
903             public void run() {\r
904                 if (c.isDisposed())\r
905                     return;\r
906                 if (changed[0]) {\r
907                     c.layout(true);\r
908                     syncRefreshScrolledComposite();\r
909                 }\r
910             }\r
911         });\r
912     }\r
913 \r
914     /**\r
915      * Filters the symbol groups and makes them visible/invisible as necessary.\r
916      * Invoke only from the SWT thread.\r
917      * \r
918      * @param text the filter text given by the client\r
919      * @return <code>true</code> if all groups were successfully filtered\r
920      *         without asynchronous results\r
921      */\r
922     boolean filterGroups(String text) {\r
923         //System.out.println("FILTERING WITH TEXT: " + text);\r
924 \r
925         String regExFilter = toPatternString(text);\r
926         Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY;\r
927 \r
928         this.currentFilterPattern = pattern;\r
929         final boolean[] changed = new boolean[] { false };\r
930         boolean filteringComplete = true;\r
931 \r
932         ViewerFilter filter = null;\r
933         if (regExFilter != null)\r
934             filter = new SymbolItemFilter(regExFilter, pattern);\r
935 \r
936         Control[] grps = c.getChildren();\r
937         for (Control ctrl : grps) {\r
938             final PGroup grp = (PGroup) ctrl;\r
939             if (grp.isDisposed())\r
940                 continue;\r
941             Boolean contentsChanged = filterGroup(grp, filter);\r
942             if (contentsChanged == null)\r
943                 filteringComplete = false;\r
944             else\r
945                 changed[0] = contentsChanged;\r
946         }\r
947 \r
948         ThreadUtils.asyncExec(swtThread, new Runnable() {\r
949             @Override\r
950             public void run() {\r
951                 if (c.isDisposed())\r
952                     return;\r
953                 if (changed[0]) {\r
954                     c.layout(true);\r
955                     syncRefreshScrolledComposite();\r
956                 }\r
957             }\r
958         });\r
959 \r
960         return filteringComplete;\r
961     }\r
962 \r
963     static boolean objectEquals(Object o1, Object o2) {\r
964         if (o1==o2) return true;\r
965         if (o1==null && o2==null) return true;\r
966         if (o1==null || o2==null) return false;\r
967         return o1.equals(o2);\r
968     }\r
969 \r
970     /**\r
971      * @param grp\r
972      * @return <code>true</code> if the filtering caused changes in the group,\r
973      *         <code>false</code> if not, and <code>null</code> if filtering\r
974      *         could not be performed yet, meaning results need to be asked\r
975      *         later\r
976      */\r
977     private Boolean filterGroup(PGroup grp, ViewerFilter filter) {\r
978         boolean changed = false;\r
979         GalleryViewer viewer = initializeGroup(grp);\r
980         if (viewer == null)\r
981             return null;\r
982 \r
983         ViewerFilter lastFilter = viewer.getFilter();\r
984 \r
985         boolean groupFiltered = Boolean.TRUE.equals(grp.getData(KEY_GROUP_FILTERED));\r
986         boolean userExpanded = Boolean.TRUE.equals(grp.getData(KEY_USER_EXPANDED));\r
987         final boolean expanded = grp.getExpanded();\r
988         final boolean visible = grp.getVisible();\r
989         final boolean filterChanged = !objectEquals(filter, lastFilter);\r
990 \r
991         // Find out how much data would be shown with the new filter.\r
992         viewer.setFilter(filter);\r
993         Object[] elements = viewer.getFilteredElements();\r
994 \r
995         ISymbolGroup symbolGroup = (ISymbolGroup) grp.getData(SymbolLibraryKeys.KEY_GROUP);\r
996         boolean filterMatchesGroup = filter != null && filter.select(viewer, null, symbolGroup);\r
997         boolean shouldBeVisible = !groupFiltered && (elements.length > 0 || filterMatchesGroup);\r
998         boolean shouldBeExpanded = shouldBeVisible && (filter != null || userExpanded);\r
999 \r
1000 //        System.out.format("%40s: visible/should be = %5s %5s,  expanded/user expanded/should be = %5s %5s %5s\n",\r
1001 //                grp.getText(),\r
1002 //                String.valueOf(visible),\r
1003 //                String.valueOf(shouldBeVisible),\r
1004 //                String.valueOf(expanded),\r
1005 //                String.valueOf(userExpanded),\r
1006 //                String.valueOf(shouldBeExpanded));\r
1007 \r
1008         if (filterChanged || visible != shouldBeVisible || expanded != shouldBeExpanded) {\r
1009             changed = true;\r
1010 \r
1011             if (shouldBeVisible == userExpanded) {\r
1012                 if (expanded != shouldBeExpanded)\r
1013                     setGroupExpandedWithoutNotification(grp, shouldBeExpanded);\r
1014                 setGroupVisible(grp, shouldBeVisible);\r
1015             } else {\r
1016                 if (filter != null) {\r
1017                     if (shouldBeVisible) {\r
1018                         // The user has not expanded this group but the group contains\r
1019                         // stuff that matches the non-empty filter => show the group.\r
1020                         setGroupExpandedWithoutNotification(grp, true);\r
1021                         setGroupVisible(grp, true);\r
1022                     } else {\r
1023                         // The user has expanded this group but it does not contain items\r
1024                         // should should be shown with the current non-empty filter => hide the group.\r
1025                         setGroupExpandedWithoutNotification(grp, true);\r
1026                         setGroupVisible(grp, false);\r
1027                     }\r
1028                 } else {\r
1029                     // All groups should be visible. Some should be expanded and others not.\r
1030                     if (expanded != userExpanded)\r
1031                         setGroupExpandedWithoutNotification(grp, userExpanded);\r
1032                     if (!visible)\r
1033                         setGroupVisible(grp, true);\r
1034                 }\r
1035             }\r
1036 \r
1037             if (shouldBeExpanded) {\r
1038                 viewer.refreshWithContent(elements);\r
1039             }\r
1040         }\r
1041 \r
1042 //        String label = grp.getText();\r
1043 //        Matcher m = pattern.matcher(label.toLowerCase());\r
1044 //        boolean visible = m.matches();\r
1045 //        if (visible != grp.getVisible()) {\r
1046 //            changed = true;\r
1047 //            setGroupVisible(grp, visible);\r
1048 //        }\r
1049 \r
1050         return changed;\r
1051     }\r
1052 \r
1053     void setGroupExpandedWithoutNotification(PGroup grp, boolean expanded) {\r
1054         // Ok, don't need to remove/add expand listener, PGroup will not notify\r
1055         // listeners when setExpanded is invoked.\r
1056         //grp.removeExpandListener(groupExpandListener);\r
1057         storeGroupExpandedState(grp, expanded);\r
1058         grp.setExpanded(expanded);\r
1059         //grp.addExpandListener(groupExpandListener);\r
1060     }\r
1061 \r
1062     void setGroupVisible(PGroup group, boolean visible) {\r
1063         GridData gd = (GridData) group.getLayoutData();\r
1064         gd.exclude = !visible;\r
1065         group.setVisible(visible);\r
1066     }\r
1067 \r
1068     boolean isGroupFiltered(String label) {\r
1069         return !currentFilterPattern.matcher(label.toLowerCase()).matches();\r
1070     }\r
1071 \r
1072     class DragSourceParticipant extends AbstractDiagramParticipant implements IDragSourceParticipant {\r
1073         @Reference  Selection selection;\r
1074         @Dependency PointerInteractor pi;\r
1075         @Dependency TransformUtil util;\r
1076         @Dependency PickContext pickContext;\r
1077 \r
1078         @Override\r
1079         public int canDrag(MouseDragBegin me) {\r
1080             if (me.button != MouseEvent.LEFT_BUTTON) return 0;\r
1081             if (getHint(Hints.KEY_TOOL) != Hints.POINTERTOOL) return 0;\r
1082             assertDependencies();\r
1083 \r
1084             PickRequest         req                     = new PickRequest(me.startCanvasPos);\r
1085             req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS;\r
1086             List<IElement>      picks                   = new ArrayList<IElement>();\r
1087             pickContext.pick(diagram, req, picks);\r
1088             Set<IElement>       sel                     = selection.getSelection(me.mouseId);\r
1089 \r
1090             if (Collections.disjoint(sel, picks)) return 0;\r
1091             // Box Select\r
1092             return DnDConstants.ACTION_COPY;\r
1093         }\r
1094 \r
1095         @Override\r
1096         public Transferable dragStart(DragGestureEvent e) {\r
1097                 \r
1098             AWTChassis chassis = (AWTChassis) e.getComponent();\r
1099             ICanvasContext cc = chassis.getCanvasContext();\r
1100             Selection sel = cc.getSingleItem(Selection.class);\r
1101 \r
1102             Set<IElement> ss = sel.getSelection(0);\r
1103             if (ss.isEmpty()) return null;\r
1104             Object[] res = new Object[ss.size()];\r
1105             int index = 0;\r
1106             for (IElement ee : ss)\r
1107                 res[index++] = ee.getHint(ElementHints.KEY_OBJECT);\r
1108 \r
1109             ISelection object = new StructuredSelection(res);\r
1110 \r
1111             LocalObjectTransferable local = new LocalObjectTransferable(object);\r
1112             \r
1113             StringBuilder json = new StringBuilder();\r
1114             json.append("{");\r
1115             json.append(" \"type\" : \"Symbol\",");\r
1116             json.append(" \"res\" : [");\r
1117             int pos = 0;\r
1118             for(int i=0;i<res.length;i++) {\r
1119                 if(pos > 0) json.append(",");\r
1120                 Object r = res[i];\r
1121                 if(r instanceof IdentifiedObject) {\r
1122                         Object id = ((IdentifiedObject) r).getId();\r
1123                         if(id instanceof IAdaptable) {\r
1124                                 Object resource = ((IAdaptable) id).getAdapter(Resource.class);\r
1125                                 if(resource != null) {\r
1126                                         long rid = ((Resource)resource).getResourceId();\r
1127                                 json.append(Long.toString(rid));\r
1128                                 pos++;\r
1129                                 }\r
1130                         }\r
1131                 }\r
1132             }\r
1133             json.append("] }");\r
1134             \r
1135             StringSelection text = new StringSelection(json.toString());\r
1136             PlaintextTransfer plainText = new PlaintextTransfer(json.toString()); \r
1137             \r
1138             return new MultiTransferable(local, text, plainText);\r
1139             \r
1140         }\r
1141 \r
1142         @Override\r
1143         public int getAllowedOps() {\r
1144             return DnDConstants.ACTION_COPY;\r
1145         }\r
1146         @Override\r
1147         public void dragDropEnd(DragSourceDropEvent dsde) {\r
1148 //            System.out.println("dragDropEnd: " + dsde);\r
1149             LocalObjectTransfer.getTransfer().clear();\r
1150         }\r
1151         @Override\r
1152         public void dragEnter(DragSourceDragEvent dsde) {\r
1153         }\r
1154         @Override\r
1155         public void dragExit(DragSourceEvent dse) {\r
1156         }\r
1157         @Override\r
1158         public void dragOver(DragSourceDragEvent dsde) {\r
1159         }\r
1160         @Override\r
1161         public void dropActionChanged(DragSourceDragEvent dsde) {\r
1162         }\r
1163     }\r
1164 \r
1165     ExpandListener groupExpandListener = new ExpandListener() {\r
1166         @Override\r
1167         public void itemCollapsed(ExpandEvent e) {\r
1168             final PGroup group = (PGroup) e.widget;\r
1169             group.setData(KEY_USER_EXPANDED, Boolean.FALSE);\r
1170             storeGroupExpandedState(group, false);\r
1171             //System.out.println("item collapsed: " + group + ", " + sc.getClientArea());\r
1172             refreshScrolledComposite();\r
1173         }\r
1174         @Override\r
1175         public void itemExpanded(ExpandEvent e) {\r
1176             final PGroup group = (PGroup) e.widget;\r
1177             group.setData(KEY_USER_EXPANDED, Boolean.TRUE);\r
1178             storeGroupExpandedState(group, true);\r
1179             //System.out.println("item expanded: " + group + ", " + sc.getClientArea());\r
1180             ThreadUtils.asyncExec(swtThread, () -> {\r
1181                 GalleryViewer viewer = initializeGroup(group);\r
1182                 if (viewer == null)\r
1183                     return;\r
1184                 ThreadUtils.asyncExec(swtThread, () -> {\r
1185                     if (viewer.getControl().isDisposed())\r
1186                         return;\r
1187                     viewer.refresh();\r
1188                     refreshScrolledComposite();\r
1189                 });\r
1190             });\r
1191         }\r
1192     };\r
1193 \r
1194     public boolean isDefaultExpanded() {\r
1195         return defaultExpanded;\r
1196     }\r
1197 \r
1198     public void setDefaultExpanded(boolean defaultExpanded) {\r
1199         this.defaultExpanded = defaultExpanded;\r
1200     }\r
1201 \r
1202     Runnable disposer(final Widget w) {\r
1203         return new Runnable() {\r
1204             @Override\r
1205             public void run() {\r
1206                 if (w.isDisposed())\r
1207                     return;\r
1208                 w.dispose();\r
1209             }\r
1210         };\r
1211     }\r
1212 \r
1213     /**\r
1214      * Invoke from SWT thread only.\r
1215      * \r
1216      * @param targetState\r
1217      */\r
1218     public void setAllExpandedStates(boolean targetState) {\r
1219         setDefaultExpanded(targetState);\r
1220         Control[] grps = c.getChildren();\r
1221         boolean changed = false;\r
1222         for (Control control : grps)\r
1223             changed |= setExpandedState((PGroup) control, targetState, false);\r
1224         if (changed)\r
1225             refreshScrolledComposite();\r
1226     }\r
1227 \r
1228     /**\r
1229      * Invoke from SWT thread only.\r
1230      * \r
1231      * @param grp\r
1232      * @param targetState\r
1233      * @return\r
1234      */\r
1235     boolean setExpandedState(PGroup grp, boolean targetState, boolean force) {\r
1236         if (grp.isDisposed())\r
1237             return false;\r
1238 \r
1239         storeGroupExpandedState(grp, targetState);\r
1240         grp.setData(KEY_USER_EXPANDED, Boolean.valueOf(targetState));\r
1241         if ((force || grp.getExpanded() != targetState) && grp.getVisible()) {\r
1242             final GalleryViewer viewer = initializeGroup(grp);\r
1243             setGroupExpandedWithoutNotification(grp, targetState);\r
1244             ThreadUtils.asyncExec(swtThread, () -> {\r
1245                 if (!grp.isDisposed()) {\r
1246                     if (viewer != null)\r
1247                         viewer.refresh();\r
1248                     refreshScrolledComposite();\r
1249                 }\r
1250             });\r
1251             return true;\r
1252         }\r
1253         return false;\r
1254     }\r
1255 \r
1256     class ImageLoader implements Runnable {\r
1257 \r
1258         private final ImageProxy  imageProxy;\r
1259         private final ISymbolItem item;\r
1260 \r
1261         public ImageLoader(ImageProxy imageProxy, ISymbolItem item) {\r
1262             this.imageProxy = imageProxy;\r
1263             this.item = item;\r
1264         }\r
1265 \r
1266         @Override\r
1267         public void run() {\r
1268             // SVG images using the SVGUniverse in SVGCache must use\r
1269             // AWT thread for all operations.\r
1270             ThreadUtils.asyncExec(AWTThread.getThreadAccess(), () -> runBlocking());\r
1271         }\r
1272 \r
1273         private void runBlocking() {\r
1274             try {\r
1275                 ISymbolGroup group = item.getGroup();\r
1276                 if (group == null)\r
1277                     throw new ProvisionException("No ISymbolGroup available for ISymbolItem " + item);\r
1278 \r
1279                 GalleryViewer viewer = groupViewers.get(group);\r
1280                 if (viewer == null) {\r
1281                     // This is normal if this composite has been disposed while these are being ran.\r
1282                     //throw new ProvisionException("No GalleryViewer available ISymbolGroup " + group);\r
1283                     imageProxy.setSource(DefaultImages.UNKNOWN2.get());\r
1284                     return;\r
1285                 }\r
1286 \r
1287                 IHintContext hints = viewer.getDiagram();\r
1288                 if (hints == null)\r
1289                     throw new ProvisionException("No diagram available for GalleryViewer of group " + group);\r
1290 \r
1291                 hints.setHint(ISymbolItem.KEY_ELEMENT_CLASS_LISTENER, new ElementClassListener(imageCache, disposed, item));\r
1292                 final ElementClass ec = item.getElementClass(hints);\r
1293 \r
1294                 // Without this the symbol library will at times\r
1295                 // not update the final graphics for the symbol.\r
1296                 // It will keep displaying the hourglass pending icon instead.\r
1297                 symbolUpdate(disposed, imageProxy, ec);\r
1298             } catch (ProvisionException e) {\r
1299                 ExceptionUtils.logWarning("Failed to provide element class for symbol item " + item, e);\r
1300                 imageProxy.setSource(DefaultImages.ERROR_DECORATOR.get());\r
1301             } catch (Exception e) {\r
1302                 ExceptionUtils.logError(e);\r
1303                 imageProxy.setSource(DefaultImages.ERROR_DECORATOR.get());\r
1304             } finally {\r
1305             }\r
1306         }\r
1307     }\r
1308 \r
1309     static class ElementClassListener implements org.simantics.db.procedure.Listener<ElementClass> {\r
1310         private Map<ISymbolItem, SoftReference<ImageProxy>> imageCache;\r
1311         private final AtomicBoolean disposed;\r
1312         private final ISymbolItem item;\r
1313 \r
1314         public ElementClassListener(Map<ISymbolItem, SoftReference<ImageProxy>> imageCache, AtomicBoolean disposed, ISymbolItem item) {\r
1315             this.imageCache = imageCache;\r
1316             this.disposed = disposed;\r
1317             this.item = item;\r
1318         }\r
1319 \r
1320         @Override\r
1321         public void execute(final ElementClass ec) {\r
1322             //System.out.println("SYMBOL CHANGED: " + item + " - disposed=" + disposed + " - " + ec);\r
1323 \r
1324             final ImageProxy[] imageProxy = { null };\r
1325             SoftReference<ImageProxy> proxyRef = imageCache.get(item);\r
1326             if (proxyRef != null)\r
1327                 imageProxy[0] = proxyRef.get();\r
1328             if (imageProxy[0] != null)\r
1329                 scheduleSymbolUpdate(disposed, imageProxy[0], ec);\r
1330         }\r
1331 \r
1332         @Override\r
1333         public void exception(Throwable t) {\r
1334             Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error in ElementClass request.", t));\r
1335         }\r
1336 \r
1337         @Override\r
1338         public boolean isDisposed() {\r
1339             //System.out.println("ElementClassListener.isDisposed " + item + " - " + disposed.get());\r
1340             return disposed.get();\r
1341         }\r
1342     }\r
1343 \r
1344     public FilterArea getFilterArea() {\r
1345         return filter;\r
1346     }\r
1347 \r
1348     public static void scheduleSymbolUpdate(final AtomicBoolean disposed, final ImageProxy imageProxy, final ElementClass ec) {\r
1349         if (disposed.get())\r
1350             return;\r
1351         ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() {\r
1352             @Override\r
1353             public void run() {\r
1354                 if (disposed.get())\r
1355                     return;\r
1356                 symbolUpdate(disposed, imageProxy, ec);\r
1357             }\r
1358         });\r
1359     }\r
1360 \r
1361     public static void symbolUpdate(final AtomicBoolean disposed, final ImageProxy imageProxy, final ElementClass ec) {\r
1362         StaticSymbol ss = ec.getSingleItem(StaticSymbol.class);\r
1363         Image source = ss == null ? DefaultImages.UNKNOWN2.get() : ss.getImage();\r
1364         imageProxy.setSource(source);\r
1365     }\r
1366 \r
1367     Runnable filterActivator = new Runnable() {\r
1368         @Override\r
1369         public void run() {\r
1370             filter.focus();\r
1371         }\r
1372     };\r
1373     Listener filterActivationListener = new Listener() {\r
1374         @Override\r
1375         public void handleEvent(Event event) {\r
1376             //System.out.println("event: " + event);\r
1377             filterActivator.run();\r
1378         }\r
1379     };\r
1380 \r
1381     ISymbolGroupListener groupListener = new ISymbolGroupListener() {\r
1382         @Override\r
1383         public void itemsChanged(ISymbolGroup group) {\r
1384             //System.out.println("symbol group changed: " + group);\r
1385             GalleryViewer viewer = groupViewers.get(group);\r
1386             if (viewer != null) {\r
1387                 ISymbolItem[] input = group.getItems();\r
1388                 viewer.setInput(input);\r
1389             }\r
1390         }\r
1391     };\r
1392 \r
1393     IEventHandler externalEventHandler = new IEventHandler() {\r
1394         @Override\r
1395         public int getEventMask() {\r
1396             return EventTypes.AnyMask;\r
1397         }\r
1398         @Override\r
1399         public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) {\r
1400             IEventHandler handler = SymbolLibraryComposite.this.eventHandler;\r
1401             return handler != null && EventTypes.passes(handler, e) ? handler.handleEvent(e) : false;\r
1402         }\r
1403     };\r
1404 \r
1405     protected volatile IEventHandler eventHandler;\r
1406 \r
1407     /**\r
1408      * @param eventHandler\r
1409      */\r
1410     public void setEventHandler(IEventHandler eventHandler) {\r
1411         this.eventHandler = eventHandler;\r
1412     }\r
1413 \r
1414     protected void storeGroupExpandedState(PGroup group, boolean expanded) {\r
1415         ISymbolGroup symbolGroup = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP);\r
1416         //System.out.println("setGroupExpandedWithoutNotification(" + group + ", " + expanded + ", " + symbolGroup + ")");\r
1417         if (symbolGroup != null) {\r
1418             Object key = symbolGroupToKey(symbolGroup);\r
1419             expandedGroups.put(key, expanded ? Boolean.TRUE : Boolean.FALSE);\r
1420         }\r
1421     }\r
1422 \r
1423     private static Object symbolGroupToKey(ISymbolGroup symbolGroup) {\r
1424         if (symbolGroup instanceof IIdentifiedObject)\r
1425             return ((IIdentifiedObject) symbolGroup).getId();\r
1426         return new Tuple2(symbolGroup.getName(), symbolGroup.getDescription());\r
1427     }\r
1428 \r
1429 }\r