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