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