]> gerrit.simantics Code Review - simantics/platform.git/blob
7d172318bf1b8561c009f2ef55086e62ee7037fc
[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.db.ReadGraph;
82 import org.simantics.db.Resource;
83 import org.simantics.db.common.procedure.adapter.ListenerAdapter;
84 import org.simantics.db.common.request.UnaryRead;
85 import org.simantics.db.exception.DatabaseException;
86 import org.simantics.diagram.internal.Activator;
87 import org.simantics.diagram.symbolcontribution.CompositeSymbolGroup;
88 import org.simantics.diagram.symbolcontribution.IIdentifiedObject;
89 import org.simantics.diagram.symbolcontribution.ISymbolProvider;
90 import org.simantics.diagram.symbolcontribution.IdentifiedObject;
91 import org.simantics.diagram.symbolcontribution.SymbolProviderFactory;
92 import org.simantics.diagram.symbollibrary.IModifiableSymbolGroup;
93 import org.simantics.diagram.symbollibrary.ISymbolGroup;
94 import org.simantics.diagram.symbollibrary.ISymbolGroupListener;
95 import org.simantics.diagram.symbollibrary.ISymbolItem;
96 import org.simantics.diagram.symbollibrary.ui.FilterConfiguration.Mode;
97 import org.simantics.diagram.synchronization.ErrorHandler;
98 import org.simantics.diagram.synchronization.LogErrorHandler;
99 import org.simantics.diagram.synchronization.SynchronizationHints;
100 import org.simantics.g2d.canvas.Hints;
101 import org.simantics.g2d.canvas.ICanvasContext;
102 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
103 import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;
104 import org.simantics.g2d.chassis.AWTChassis;
105 import org.simantics.g2d.diagram.DiagramUtils;
106 import org.simantics.g2d.diagram.handler.PickContext;
107 import org.simantics.g2d.diagram.handler.PickRequest;
108 import org.simantics.g2d.diagram.handler.layout.FlowLayout;
109 import org.simantics.g2d.diagram.participant.AbstractDiagramParticipant;
110 import org.simantics.g2d.diagram.participant.Selection;
111 import org.simantics.g2d.diagram.participant.pointertool.PointerInteractor;
112 import org.simantics.g2d.dnd.IDragSourceParticipant;
113 import org.simantics.g2d.element.ElementClass;
114 import org.simantics.g2d.element.ElementHints;
115 import org.simantics.g2d.element.IElement;
116 import org.simantics.g2d.element.handler.StaticSymbol;
117 import org.simantics.g2d.event.adapter.SWTMouseEventAdapter;
118 import org.simantics.g2d.gallery.GalleryViewer;
119 import org.simantics.g2d.gallery.ILabelProvider;
120 import org.simantics.g2d.image.DefaultImages;
121 import org.simantics.g2d.image.Image;
122 import org.simantics.g2d.image.Image.Feature;
123 import org.simantics.g2d.image.impl.ImageProxy;
124 import org.simantics.g2d.participant.TransformUtil;
125 import org.simantics.scenegraph.g2d.events.EventTypes;
126 import org.simantics.scenegraph.g2d.events.IEventHandler;
127 import org.simantics.scenegraph.g2d.events.MouseEvent;
128 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent;
129 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
130 import org.simantics.scl.runtime.tuple.Tuple2;
131 import org.simantics.ui.SimanticsUI;
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         SimanticsUI.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
991         // Find out how much data would be shown with the new filter.
992         viewer.setFilter(filter);
993         Object[] elements = viewer.getFilteredElements();
994
995         ISymbolGroup symbolGroup = (ISymbolGroup) grp.getData(SymbolLibraryKeys.KEY_GROUP);
996         boolean filterMatchesGroup = filter != null && filter.select(viewer, null, symbolGroup);
997         boolean shouldBeVisible = !groupFiltered && (elements.length > 0 || filterMatchesGroup);
998         boolean shouldBeExpanded = shouldBeVisible && (filter != null || userExpanded);
999
1000 //        System.out.format("%40s: visible/should be = %5s %5s,  expanded/user expanded/should be = %5s %5s %5s\n",
1001 //                grp.getText(),
1002 //                String.valueOf(visible),
1003 //                String.valueOf(shouldBeVisible),
1004 //                String.valueOf(expanded),
1005 //                String.valueOf(userExpanded),
1006 //                String.valueOf(shouldBeExpanded));
1007
1008         if (filterChanged || visible != shouldBeVisible || expanded != shouldBeExpanded) {
1009             changed = true;
1010
1011             if (shouldBeVisible == userExpanded) {
1012                 if (expanded != shouldBeExpanded)
1013                     setGroupExpandedWithoutNotification(grp, shouldBeExpanded);
1014                 setGroupVisible(grp, shouldBeVisible);
1015             } else {
1016                 if (filter != null) {
1017                     if (shouldBeVisible) {
1018                         // The user has not expanded this group but the group contains
1019                         // stuff that matches the non-empty filter => show the group.
1020                         setGroupExpandedWithoutNotification(grp, true);
1021                         setGroupVisible(grp, true);
1022                     } else {
1023                         // The user has expanded this group but it does not contain items
1024                         // should should be shown with the current non-empty filter => hide the group.
1025                         setGroupExpandedWithoutNotification(grp, true);
1026                         setGroupVisible(grp, false);
1027                     }
1028                 } else {
1029                     // All groups should be visible. Some should be expanded and others not.
1030                     if (expanded != userExpanded)
1031                         setGroupExpandedWithoutNotification(grp, userExpanded);
1032                     if (!visible)
1033                         setGroupVisible(grp, true);
1034                 }
1035             }
1036
1037             if (shouldBeExpanded) {
1038                 viewer.refreshWithContent(elements);
1039             }
1040         }
1041
1042 //        String label = grp.getText();
1043 //        Matcher m = pattern.matcher(label.toLowerCase());
1044 //        boolean visible = m.matches();
1045 //        if (visible != grp.getVisible()) {
1046 //            changed = true;
1047 //            setGroupVisible(grp, visible);
1048 //        }
1049
1050         return changed;
1051     }
1052
1053     void setGroupExpandedWithoutNotification(PGroup grp, boolean expanded) {
1054         // Ok, don't need to remove/add expand listener, PGroup will not notify
1055         // listeners when setExpanded is invoked.
1056         //grp.removeExpandListener(groupExpandListener);
1057         storeGroupExpandedState(grp, expanded);
1058         grp.setExpanded(expanded);
1059         //grp.addExpandListener(groupExpandListener);
1060     }
1061
1062     void setGroupVisible(PGroup group, boolean visible) {
1063         GridData gd = (GridData) group.getLayoutData();
1064         gd.exclude = !visible;
1065         group.setVisible(visible);
1066     }
1067
1068     boolean isGroupFiltered(String label) {
1069         return !currentFilterPattern.matcher(label.toLowerCase()).matches();
1070     }
1071
1072     class DragSourceParticipant extends AbstractDiagramParticipant implements IDragSourceParticipant {
1073         @Reference  Selection selection;
1074         @Dependency PointerInteractor pi;
1075         @Dependency TransformUtil util;
1076         @Dependency PickContext pickContext;
1077
1078         @Override
1079         public int canDrag(MouseDragBegin me) {
1080             if (me.button != MouseEvent.LEFT_BUTTON) return 0;
1081             if (getHint(Hints.KEY_TOOL) != Hints.POINTERTOOL) return 0;
1082             assertDependencies();
1083
1084             PickRequest         req                     = new PickRequest(me.startCanvasPos);
1085             req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS;
1086             List<IElement>      picks                   = new ArrayList<IElement>();
1087             pickContext.pick(diagram, req, picks);
1088             Set<IElement>       sel                     = selection.getSelection(me.mouseId);
1089
1090             if (Collections.disjoint(sel, picks)) return 0;
1091             // Box Select
1092             return DnDConstants.ACTION_COPY;
1093         }
1094
1095         @Override
1096         public Transferable dragStart(DragGestureEvent e) {
1097                 
1098             AWTChassis chassis = (AWTChassis) e.getComponent();
1099             ICanvasContext cc = chassis.getCanvasContext();
1100             Selection sel = cc.getSingleItem(Selection.class);
1101
1102             Set<IElement> ss = sel.getSelection(0);
1103             if (ss.isEmpty()) return null;
1104             Object[] res = new Object[ss.size()];
1105             int index = 0;
1106             for (IElement ee : ss)
1107                 res[index++] = ee.getHint(ElementHints.KEY_OBJECT);
1108
1109             ISelection object = new StructuredSelection(res);
1110
1111             LocalObjectTransferable local = new LocalObjectTransferable(object);
1112             
1113             StringBuilder json = new StringBuilder();
1114             json.append("{");
1115             json.append(" \"type\" : \"Symbol\",");
1116             json.append(" \"res\" : [");
1117             int pos = 0;
1118             for(int i=0;i<res.length;i++) {
1119                 if(pos > 0) json.append(",");
1120                 Object r = res[i];
1121                 if(r instanceof IdentifiedObject) {
1122                         Object id = ((IdentifiedObject) r).getId();
1123                         if(id instanceof IAdaptable) {
1124                                 Object resource = ((IAdaptable) id).getAdapter(Resource.class);
1125                                 if(resource != null) {
1126                                         long rid = ((Resource)resource).getResourceId();
1127                                 json.append(Long.toString(rid));
1128                                 pos++;
1129                                 }
1130                         }
1131                 }
1132             }
1133             json.append("] }");
1134             
1135             StringSelection text = new StringSelection(json.toString());
1136             PlaintextTransfer plainText = new PlaintextTransfer(json.toString()); 
1137             
1138             return new MultiTransferable(local, text, plainText);
1139             
1140         }
1141
1142         @Override
1143         public int getAllowedOps() {
1144             return DnDConstants.ACTION_COPY;
1145         }
1146         @Override
1147         public void dragDropEnd(DragSourceDropEvent dsde) {
1148 //            System.out.println("dragDropEnd: " + dsde);
1149             LocalObjectTransfer.getTransfer().clear();
1150         }
1151         @Override
1152         public void dragEnter(DragSourceDragEvent dsde) {
1153         }
1154         @Override
1155         public void dragExit(DragSourceEvent dse) {
1156         }
1157         @Override
1158         public void dragOver(DragSourceDragEvent dsde) {
1159         }
1160         @Override
1161         public void dropActionChanged(DragSourceDragEvent dsde) {
1162         }
1163     }
1164
1165     ExpandListener groupExpandListener = new ExpandListener() {
1166         @Override
1167         public void itemCollapsed(ExpandEvent e) {
1168             final PGroup group = (PGroup) e.widget;
1169             group.setData(KEY_USER_EXPANDED, Boolean.FALSE);
1170             storeGroupExpandedState(group, false);
1171             //System.out.println("item collapsed: " + group + ", " + sc.getClientArea());
1172             refreshScrolledComposite();
1173         }
1174         @Override
1175         public void itemExpanded(ExpandEvent e) {
1176             final PGroup group = (PGroup) e.widget;
1177             group.setData(KEY_USER_EXPANDED, Boolean.TRUE);
1178             storeGroupExpandedState(group, true);
1179             //System.out.println("item expanded: " + group + ", " + sc.getClientArea());
1180             ThreadUtils.asyncExec(swtThread, () -> {
1181                 GalleryViewer viewer = initializeGroup(group);
1182                 if (viewer == null)
1183                     return;
1184                 ThreadUtils.asyncExec(swtThread, () -> {
1185                     if (viewer.getControl().isDisposed())
1186                         return;
1187                     viewer.refresh();
1188                     refreshScrolledComposite();
1189                 });
1190             });
1191         }
1192     };
1193
1194     public boolean isDefaultExpanded() {
1195         return defaultExpanded;
1196     }
1197
1198     public void setDefaultExpanded(boolean defaultExpanded) {
1199         this.defaultExpanded = defaultExpanded;
1200     }
1201
1202     Runnable disposer(final Widget w) {
1203         return new Runnable() {
1204             @Override
1205             public void run() {
1206                 if (w.isDisposed())
1207                     return;
1208                 w.dispose();
1209             }
1210         };
1211     }
1212
1213     /**
1214      * Invoke from SWT thread only.
1215      * 
1216      * @param targetState
1217      */
1218     public void setAllExpandedStates(boolean targetState) {
1219         setDefaultExpanded(targetState);
1220         Control[] grps = c.getChildren();
1221         boolean changed = false;
1222         for (Control control : grps)
1223             changed |= setExpandedState((PGroup) control, targetState, false);
1224         if (changed)
1225             refreshScrolledComposite();
1226     }
1227
1228     /**
1229      * Invoke from SWT thread only.
1230      * 
1231      * @param grp
1232      * @param targetState
1233      * @return
1234      */
1235     boolean setExpandedState(PGroup grp, boolean targetState, boolean force) {
1236         if (grp.isDisposed())
1237             return false;
1238
1239         storeGroupExpandedState(grp, targetState);
1240         grp.setData(KEY_USER_EXPANDED, Boolean.valueOf(targetState));
1241         if ((force || grp.getExpanded() != targetState) && grp.getVisible()) {
1242             final GalleryViewer viewer = initializeGroup(grp);
1243             setGroupExpandedWithoutNotification(grp, targetState);
1244             ThreadUtils.asyncExec(swtThread, () -> {
1245                 if (!grp.isDisposed()) {
1246                     if (viewer != null)
1247                         viewer.refresh();
1248                     refreshScrolledComposite();
1249                 }
1250             });
1251             return true;
1252         }
1253         return false;
1254     }
1255
1256     class ImageLoader implements Runnable {
1257
1258         private final ImageProxy  imageProxy;
1259         private final ISymbolItem item;
1260
1261         public ImageLoader(ImageProxy imageProxy, ISymbolItem item) {
1262             this.imageProxy = imageProxy;
1263             this.item = item;
1264         }
1265
1266         @Override
1267         public void run() {
1268             // SVG images using the SVGUniverse in SVGCache must use
1269             // AWT thread for all operations.
1270             ThreadUtils.asyncExec(AWTThread.getThreadAccess(), () -> runBlocking());
1271         }
1272
1273         private void runBlocking() {
1274             try {
1275                 ISymbolGroup group = item.getGroup();
1276                 if (group == null)
1277                     throw new ProvisionException("No ISymbolGroup available for ISymbolItem " + item);
1278
1279                 GalleryViewer viewer = groupViewers.get(group);
1280                 if (viewer == null) {
1281                     // This is normal if this composite has been disposed while these are being ran.
1282                     //throw new ProvisionException("No GalleryViewer available ISymbolGroup " + group);
1283                     imageProxy.setSource(DefaultImages.UNKNOWN2.get());
1284                     return;
1285                 }
1286
1287                 IHintContext hints = viewer.getDiagram();
1288                 if (hints == null)
1289                     throw new ProvisionException("No diagram available for GalleryViewer of group " + group);
1290
1291                 hints.setHint(ISymbolItem.KEY_ELEMENT_CLASS_LISTENER, new ElementClassListener(imageCache, disposed, item));
1292                 final ElementClass ec = item.getElementClass(hints);
1293
1294                 // Without this the symbol library will at times
1295                 // not update the final graphics for the symbol.
1296                 // It will keep displaying the hourglass pending icon instead.
1297                 symbolUpdate(disposed, imageProxy, ec);
1298             } catch (ProvisionException e) {
1299                 ExceptionUtils.logWarning("Failed to provide element class for symbol item " + item, e);
1300                 imageProxy.setSource(DefaultImages.ERROR_DECORATOR.get());
1301             } catch (Exception e) {
1302                 ExceptionUtils.logError(e);
1303                 imageProxy.setSource(DefaultImages.ERROR_DECORATOR.get());
1304             } finally {
1305             }
1306         }
1307     }
1308
1309     static class ElementClassListener implements org.simantics.db.procedure.Listener<ElementClass> {
1310         private Map<ISymbolItem, SoftReference<ImageProxy>> imageCache;
1311         private final AtomicBoolean disposed;
1312         private final ISymbolItem item;
1313
1314         public ElementClassListener(Map<ISymbolItem, SoftReference<ImageProxy>> imageCache, AtomicBoolean disposed, ISymbolItem item) {
1315             this.imageCache = imageCache;
1316             this.disposed = disposed;
1317             this.item = item;
1318         }
1319
1320         @Override
1321         public void execute(final ElementClass ec) {
1322             //System.out.println("SYMBOL CHANGED: " + item + " - disposed=" + disposed + " - " + ec);
1323
1324             final ImageProxy[] imageProxy = { null };
1325             SoftReference<ImageProxy> proxyRef = imageCache.get(item);
1326             if (proxyRef != null)
1327                 imageProxy[0] = proxyRef.get();
1328             if (imageProxy[0] != null)
1329                 scheduleSymbolUpdate(disposed, imageProxy[0], ec);
1330         }
1331
1332         @Override
1333         public void exception(Throwable t) {
1334             Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error in ElementClass request.", t));
1335         }
1336
1337         @Override
1338         public boolean isDisposed() {
1339             //System.out.println("ElementClassListener.isDisposed " + item + " - " + disposed.get());
1340             return disposed.get();
1341         }
1342     }
1343
1344     public FilterArea getFilterArea() {
1345         return filter;
1346     }
1347
1348     public static void scheduleSymbolUpdate(final AtomicBoolean disposed, final ImageProxy imageProxy, final ElementClass ec) {
1349         if (disposed.get())
1350             return;
1351         ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() {
1352             @Override
1353             public void run() {
1354                 if (disposed.get())
1355                     return;
1356                 symbolUpdate(disposed, imageProxy, ec);
1357             }
1358         });
1359     }
1360
1361     public static void symbolUpdate(final AtomicBoolean disposed, final ImageProxy imageProxy, final ElementClass ec) {
1362         StaticSymbol ss = ec.getSingleItem(StaticSymbol.class);
1363         Image source = ss == null ? DefaultImages.UNKNOWN2.get() : ss.getImage();
1364         imageProxy.setSource(source);
1365     }
1366
1367     Runnable filterActivator = new Runnable() {
1368         @Override
1369         public void run() {
1370             filter.focus();
1371         }
1372     };
1373     Listener filterActivationListener = new Listener() {
1374         @Override
1375         public void handleEvent(Event event) {
1376             //System.out.println("event: " + event);
1377             filterActivator.run();
1378         }
1379     };
1380
1381     ISymbolGroupListener groupListener = new ISymbolGroupListener() {
1382         @Override
1383         public void itemsChanged(ISymbolGroup group) {
1384             //System.out.println("symbol group changed: " + group);
1385             GalleryViewer viewer = groupViewers.get(group);
1386             if (viewer != null) {
1387                 ISymbolItem[] input = group.getItems();
1388                 viewer.setInput(input);
1389             }
1390         }
1391     };
1392
1393     IEventHandler externalEventHandler = new IEventHandler() {
1394         @Override
1395         public int getEventMask() {
1396             return EventTypes.AnyMask;
1397         }
1398         @Override
1399         public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) {
1400             IEventHandler handler = SymbolLibraryComposite.this.eventHandler;
1401             return handler != null && EventTypes.passes(handler, e) ? handler.handleEvent(e) : false;
1402         }
1403     };
1404
1405     protected volatile IEventHandler eventHandler;
1406
1407     /**
1408      * @param eventHandler
1409      */
1410     public void setEventHandler(IEventHandler eventHandler) {
1411         this.eventHandler = eventHandler;
1412     }
1413
1414     protected void storeGroupExpandedState(PGroup group, boolean expanded) {
1415         ISymbolGroup symbolGroup = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP);
1416         //System.out.println("setGroupExpandedWithoutNotification(" + group + ", " + expanded + ", " + symbolGroup + ")");
1417         if (symbolGroup != null) {
1418             Object key = symbolGroupToKey(symbolGroup);
1419             expandedGroups.put(key, expanded ? Boolean.TRUE : Boolean.FALSE);
1420         }
1421     }
1422
1423     private static Object symbolGroupToKey(ISymbolGroup symbolGroup) {
1424         if (symbolGroup instanceof IIdentifiedObject)
1425             return ((IIdentifiedObject) symbolGroup).getId();
1426         return new Tuple2(symbolGroup.getName(), symbolGroup.getDescription());
1427     }
1428
1429 }