1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
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
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.diagram.symbollibrary.ui;
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;
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;
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;
146 * @author Tuukka Lehtonen
148 public class SymbolLibraryComposite extends Composite {
150 private static final int FILTER_DELAY = 500;
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";
156 /** Root composite */
157 ScrolledComposite sc;
159 IThreadWorkQueue swtThread;
160 boolean defaultExpanded = false;
161 ISymbolProvider symbolProvider;
162 AtomicBoolean disposed = new AtomicBoolean(false);
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.
171 AtomicInteger loadCount = new AtomicInteger();
173 Map<ISymbolGroup, PGroup> groups = new HashMap<>();
174 Map<ISymbolGroup, GalleryViewer> groupViewers = new HashMap<>();
175 Map<Object, Boolean> expandedGroups = new HashMap<>();
176 LocalResourceManager resourceManager;
179 ThreadFactory threadFactory = new ThreadFactory() {
181 public Thread newThread(Runnable r) {
182 Thread t = new Thread(r, "Symbol Library Loader");
184 t.setPriority(Thread.NORM_PRIORITY);
189 Semaphore loaderSemaphore = new Semaphore(1);
190 ExecutorService loaderExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
191 2L, TimeUnit.SECONDS,
192 new SynchronousQueue<Runnable>(),
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)}.
200 Map<ISymbolItem, SoftReference<ImageProxy>> imageCache = new WeakHashMap<ISymbolItem, SoftReference<ImageProxy>>();
202 static final Pattern ANY = Pattern.compile(".*");
203 Pattern currentFilterPattern = ANY;
205 FilterConfiguration config = new FilterConfiguration();
206 IFilter currentGroupFilter = AcceptAllFilter.getInstance();
208 ErrorHandler errorHandler = LogErrorHandler.INSTANCE;
210 static class GroupDescriptor {
211 public final ISymbolGroup lib;
212 public final String label;
213 public final String description;
214 public final PGroup group;
216 public GroupDescriptor(ISymbolGroup lib, String label, String description, PGroup group) {
218 assert(label != null);
221 this.description = description;
226 Comparator<GroupDescriptor> groupComparator = new Comparator<GroupDescriptor>() {
228 public int compare(GroupDescriptor o1, GroupDescriptor o2) {
229 return o1.label.compareToIgnoreCase(o2.label);
233 static final EnumSet<Feature> VOLATILE = EnumSet.of(Feature.Volatile);
235 static class PendingImage extends ImageProxy {
236 EnumSet<Feature> features;
237 PendingImage(Image source, EnumSet<Feature> features) {
239 this.features = features;
242 public EnumSet<Feature> getFeatures() {
247 class LabelProvider extends BaseLabelProvider implements ILabelProvider {
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();
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);
264 public String getText(final Object element) {
265 return ((ISymbolItem) element).getName();
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;
276 public java.awt.Image getToolTipImage(Object object) {
280 public Color getToolTipBackgroundColor(Object object) {
285 public Color getToolTipForegroundColor(Object object) {
290 public SymbolLibraryComposite(final Composite parent, int style, SymbolProviderFactory symbolProvider) {
291 super(parent, style);
293 SimanticsUI.getSession().asyncRequest(new CreateSymbolProvider(symbolProvider), new SymbolProviderListener());
294 addDisposeListener(new DisposeListener() {
296 public void widgetDisposed(DisposeEvent e) {
305 static class CreateSymbolProvider extends UnaryRead<SymbolProviderFactory, ISymbolProvider> {
306 public CreateSymbolProvider(SymbolProviderFactory factory) {
310 public ISymbolProvider perform(ReadGraph graph) throws DatabaseException {
311 //System.out.println("CreateSymbolProvider.perform: " + parameter);
312 ISymbolProvider provider = parameter.create(graph);
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);
334 class SymbolProviderListener extends ListenerAdapter<ISymbolProvider> {
336 public void exception(Throwable t) {
337 ErrorLogger.defaultLogError(t);
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();
349 public boolean isDisposed() {
350 boolean result = SymbolLibraryComposite.this.isDisposed();
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));
359 this.resourceManager = new LocalResourceManager(JFaceResources.getResources(getDisplay()), this);
360 swtThread = SWTThread.getThreadAccess(this);
362 filter = new FilterArea(this, SWT.NONE);
363 GridDataFactory.fillDefaults().grab(true, false).applyTo(filter);
364 filter.getText().addModifyListener(new ModifyListener() {
366 //long lastModificationTime = -1000;
368 public void modifyText(ModifyEvent e) {
369 scheduleDelayedFilter(FILTER_DELAY, TimeUnit.MILLISECONDS);
371 private void scheduleDelayedFilter(long filterDelay, TimeUnit delayUnit) {
372 final String text = filter.getText().getText();
374 //long time = System.currentTimeMillis();
375 //long delta = time - lastModificationTime;
376 //lastModificationTime = time;
378 final int count = ++modCount;
379 ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
382 int newCount = modCount;
383 if (newCount != count)
386 ThreadUtils.asyncExec(swtThread, new Runnable() {
391 if (!filterGroups(text)) {
392 scheduleDelayedFilter(100, TimeUnit.MILLISECONDS);
397 }, filterDelay, delayUnit);
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() {
410 public void controlResized(ControlEvent e) {
411 //System.out.println("ScrolledComposite resized: " + sc.getSize());
412 refreshScrolledComposite();
415 //sc.setBackground(sc.getDisplay().getSystemColor(SWT.COLOR_RED));
417 c = new Composite(sc, 0);
419 GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(c);
420 //c.setBackground(c.getDisplay().getSystemColor(SWT.COLOR_BLUE));
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);
429 c.addDisposeListener(new DisposeListener() {
431 public void widgetDisposed(DisposeEvent e) {
432 // Remember to shutdown the executor
433 loaderExecutor.shutdown();
434 groupViewers.clear();
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() {
449 syncRefreshScrolledComposite();
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);
466 * (Re-)Load symbol groups, refresh the content
468 void load(Collection<ISymbolGroup> _libraries) {
469 if (_libraries == null)
470 _libraries = Collections.emptyList();
471 final Collection<ISymbolGroup> libraries = _libraries;
472 if (loaderExecutor.isShutdown())
474 loaderExecutor.execute(new Runnable() {
477 // Increment loadCount to signal that a new load cycle is on the way.
478 Integer loadId = loadCount.incrementAndGet();
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);
488 loaderSemaphore.release();
489 ExceptionUtils.logAndShowError(e);
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();
501 groupViewers.remove(entry.getKey());
502 if (group != null && !group.isDisposed())
503 ThreadUtils.asyncExec(swtThread, disposer(group));
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));
515 // Populate all the missing groups.
516 IFilter groupFilter = currentGroupFilter;
520 groupDescs.iterator(),
526 loaderSemaphore.release();
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)
542 // Check whether to still continue this population or not.
543 int currentLoadId = loadCount.get();
544 if (currentLoadId != loadId) {
549 if (!iter.hasNext()) {
550 ThreadUtils.asyncExec(swtThread, new Runnable() {
553 if (filter.isDisposed() || c.isDisposed())
563 final GroupDescriptor desc = iter.next();
565 ThreadUtils.asyncExec(swtThread, new Runnable() {
568 // Must make sure that loadComplete is invoked under error
572 } catch (RuntimeException e) {
574 ExceptionUtils.logAndShowError(e);
577 ExceptionUtils.logAndShowError(e);
581 public void populateGroup() {
582 if (c.isDisposed()) {
587 //System.out.println("populating: " + desc.label);
588 PGroup group = desc.group;
589 Runnable chainedCompletionCallback = loadComplete;
590 if (group == null || group.isDisposed()) {
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);
604 group.moveAbove(null);
607 installMouseEventAdapter(group, new SWTMouseEventAdapter(group, externalEventHandler));
609 groups.put(desc.lib, group);
611 Boolean shouldBeExpanded = expandedGroups.get(symbolGroupToKey(desc.lib));
612 if (shouldBeExpanded == null)
613 shouldBeExpanded = defaultExpanded;
614 group.setData(KEY_USER_EXPANDED, shouldBeExpanded);
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);
622 // Track group content changes if possible.
623 if (desc.lib instanceof IModifiableSymbolGroup) {
624 IModifiableSymbolGroup mod = (IModifiableSymbolGroup) desc.lib;
625 mod.addListener(groupListener);
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));
639 group.setData(SymbolLibraryKeys.KEY_GROUP, desc.lib);
640 group.setText(desc.label);
641 group.setToolTipText(desc.description);
643 // Hide the group immediately if necessary.
644 boolean groupFiltered = !groupFilter.select(desc.label);
645 group.setData(KEY_GROUP_FILTERED, Boolean.valueOf(groupFiltered));
647 setGroupVisible(group, false);
649 syncRefreshScrolledComposite();
651 final PGroup group_ = group;
652 Runnable newCompletionCallback = chainedCompletionCallback;
654 populateGroups(exec, group_, iter, groupFilter, loadId, newCompletionCallback);
660 protected void installMouseEventAdapter(Control onControl, SWTMouseEventAdapter eventAdapter) {
661 onControl.addMouseListener(eventAdapter);
662 onControl.addMouseTrackListener(eventAdapter);
663 onControl.addMouseMoveListener(eventAdapter);
664 onControl.addMouseWheelListener(eventAdapter);
669 * @return <code>null</code> if GalleryViewer is currently being created
671 GalleryViewer initializeGroup(final PGroup group) {
672 if (group.isDisposed())
675 //System.out.println("initializeGroup(" + group.getText() + ")");
677 synchronized (group) {
678 if (group.getData(KEY_VIEWER_INITIALIZED) != null) {
679 return (GalleryViewer) group.getData(SymbolLibraryKeys.KEY_GALLERY);
681 group.setData(KEY_VIEWER_INITIALIZED, Boolean.TRUE);
684 //System.out.println("initializing group: " + group.getText());
686 // NOTE: this will NOT stop to wait until the SWT/AWT UI
687 // population has been completed.
688 GalleryViewer viewer = new GalleryViewer(group);
690 ISymbolGroup input = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP);
691 initializeViewer(group, input, viewer);
693 groupViewers.put(input, viewer);
694 group.setData(SymbolLibraryKeys.KEY_GALLERY, viewer);
696 //System.out.println("initialized group: " + group.getText());
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);
707 viewer.setContentProvider(new IStructuredContentProvider() {
710 * Returns the elements in the input, which must be either an array or a
711 * <code>Collection</code>.
714 public Object[] getElements(Object inputElement) {
715 if(inputElement == null) return new Object[0];
716 return ((ISymbolGroup)inputElement).getItems();
720 * This implementation does nothing.
723 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
728 * This implementation does nothing.
731 public void dispose() {
736 viewer.setLabelProvider(new LabelProvider());
737 viewer.setInput(input);
739 // Add event handler that closes libraries on double clicks into empty
741 viewer.getCanvasContext().getEventHandlerStack().add(new IEventHandler() {
743 public int getEventMask() {
744 return EventTypes.MouseDoubleClickMask;
748 public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) {
749 if (externalEventHandler.handleEvent(e))
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())
759 //System.out.println("NOTHING CLICKED");
760 if (group.isDisposed())
762 group.getDisplay().asyncExec(() -> {
763 if (group.isDisposed())
766 boolean exp = !group.getExpanded();
767 group.setData(KEY_USER_EXPANDED, Boolean.valueOf(exp));
768 setGroupExpandedWithoutNotification(group, exp);
769 refreshScrolledComposite();
778 static String toPatternString(String filter) {
779 return DefaultFilterStrategy.defaultToPatternString(filter, true);
782 static class SymbolItemFilter extends ViewerFilter {
783 private final String string;
784 private final Matcher m;
786 public SymbolItemFilter(String string, Pattern pattern) {
787 this.string = string;
788 this.m = pattern.matcher("");
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());
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"));
811 public int hashCode() {
812 return string == null ? 0 : string.hashCode();
816 public boolean equals(Object obj) {
821 if (getClass() != obj.getClass())
823 SymbolItemFilter other = (SymbolItemFilter) obj;
824 if (string == null) {
825 if (other.string != null)
827 } else if (!string.equals(other.string))
833 static Pattern toPattern(String filterText) {
834 String regExFilter = toPatternString(filterText);
835 Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY;
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()) {
844 patterns.add(toPattern(f.getFilterText()));
846 return new IFilter() {
848 public boolean select(Object toTest) {
849 if (patterns.isEmpty())
852 String s = (String) toTest;
855 for (Pattern pat : patterns) {
856 Matcher m = pat.matcher(s.toLowerCase());
857 //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL"));
863 for (Pattern pat : patterns) {
864 Matcher m = pat.matcher(s.toLowerCase());
865 //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL"));
871 throw new Error("Shouldn't happen");
877 void updateFilterConfiguration(FilterConfiguration config) {
878 this.config = config;
879 IFilter filter = composeFilter(config);
880 this.currentGroupFilter = filter;
883 void applyGroupFilters() {
884 IFilter groupFilter = this.currentGroupFilter;
885 final boolean[] changed = new boolean[] { false };
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;
895 grp.setData(KEY_GROUP_FILTERED, Boolean.valueOf(!shouldBeVisible));
897 setGroupVisible(grp, shouldBeVisible);
901 ThreadUtils.asyncExec(swtThread, new Runnable() {
908 syncRefreshScrolledComposite();
915 * Filters the symbol groups and makes them visible/invisible as necessary.
916 * Invoke only from the SWT thread.
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
922 boolean filterGroups(String text) {
923 //System.out.println("FILTERING WITH TEXT: " + text);
925 String regExFilter = toPatternString(text);
926 Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY;
928 this.currentFilterPattern = pattern;
929 final boolean[] changed = new boolean[] { false };
930 boolean filteringComplete = true;
932 ViewerFilter filter = null;
933 if (regExFilter != null)
934 filter = new SymbolItemFilter(regExFilter, pattern);
936 Control[] grps = c.getChildren();
937 for (Control ctrl : grps) {
938 final PGroup grp = (PGroup) ctrl;
939 if (grp.isDisposed())
941 Boolean contentsChanged = filterGroup(grp, filter);
942 if (contentsChanged == null)
943 filteringComplete = false;
945 changed[0] = contentsChanged;
948 ThreadUtils.asyncExec(swtThread, new Runnable() {
955 syncRefreshScrolledComposite();
960 return filteringComplete;
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);
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
977 private Boolean filterGroup(PGroup grp, ViewerFilter filter) {
978 boolean changed = false;
979 GalleryViewer viewer = initializeGroup(grp);
983 ViewerFilter lastFilter = viewer.getFilter();
985 boolean groupFiltered = Boolean.TRUE.equals(grp.getData(KEY_GROUP_FILTERED));
986 boolean userExpanded = Boolean.TRUE.equals(grp.getData(KEY_USER_EXPANDED));
987 final boolean expanded = grp.getExpanded();
988 final boolean visible = grp.getVisible();
989 final boolean filterChanged = !objectEquals(filter, lastFilter);
990 final ISymbolGroup symbolGroup = (ISymbolGroup) grp.getData(SymbolLibraryKeys.KEY_GROUP);
991 final boolean filterMatchesGroup = filter != null && filter.select(viewer, null, symbolGroup);
993 // Find out how much data would be shown with the new filter.
994 viewer.setFilter(filterMatchesGroup ? null : filter);
995 Object[] elements = viewer.getFilteredElements();
997 boolean shouldBeVisible = !groupFiltered && (elements.length > 0 || filterMatchesGroup);
998 boolean shouldBeExpanded = shouldBeVisible && (filter != null || userExpanded);
1000 // System.out.format("%40s: filterMatchesGroup(%s) = %s, visible/should be = %5s %5s, expanded/user expanded/should be = %5s %5s %5s\n",
1002 // symbolGroup.getName(),
1003 // String.valueOf(filterMatchesGroup),
1004 // String.valueOf(visible),
1005 // String.valueOf(shouldBeVisible),
1006 // String.valueOf(expanded),
1007 // String.valueOf(userExpanded),
1008 // String.valueOf(shouldBeExpanded));
1010 if (filterChanged || visible != shouldBeVisible || expanded != shouldBeExpanded) {
1013 if (shouldBeVisible == userExpanded) {
1014 if (expanded != shouldBeExpanded)
1015 setGroupExpandedWithoutNotification(grp, shouldBeExpanded);
1016 setGroupVisible(grp, shouldBeVisible);
1018 if (filter != null) {
1019 if (shouldBeVisible) {
1020 // The user has not expanded this group but the group contains
1021 // stuff that matches the non-empty filter => show the group.
1022 setGroupExpandedWithoutNotification(grp, true);
1023 setGroupVisible(grp, true);
1025 // The user has expanded this group but it does not contain items
1026 // should should be shown with the current non-empty filter => hide the group.
1027 setGroupExpandedWithoutNotification(grp, true);
1028 setGroupVisible(grp, false);
1031 // All groups should be visible. Some should be expanded and others not.
1032 if (expanded != userExpanded)
1033 setGroupExpandedWithoutNotification(grp, userExpanded);
1035 setGroupVisible(grp, true);
1039 if (shouldBeExpanded) {
1040 viewer.refreshWithContent(elements);
1044 // String label = grp.getText();
1045 // Matcher m = pattern.matcher(label.toLowerCase());
1046 // boolean visible = m.matches();
1047 // if (visible != grp.getVisible()) {
1049 // setGroupVisible(grp, visible);
1055 void setGroupExpandedWithoutNotification(PGroup grp, boolean expanded) {
1056 // Ok, don't need to remove/add expand listener, PGroup will not notify
1057 // listeners when setExpanded is invoked.
1058 //grp.removeExpandListener(groupExpandListener);
1059 storeGroupExpandedState(grp, expanded);
1060 grp.setExpanded(expanded);
1061 //grp.addExpandListener(groupExpandListener);
1064 void setGroupVisible(PGroup group, boolean visible) {
1065 GridData gd = (GridData) group.getLayoutData();
1066 gd.exclude = !visible;
1067 group.setVisible(visible);
1070 boolean isGroupFiltered(String label) {
1071 return !currentFilterPattern.matcher(label.toLowerCase()).matches();
1074 class DragSourceParticipant extends AbstractDiagramParticipant implements IDragSourceParticipant {
1075 @Reference Selection selection;
1076 @Dependency PointerInteractor pi;
1077 @Dependency TransformUtil util;
1078 @Dependency PickContext pickContext;
1081 public int canDrag(MouseDragBegin me) {
1082 if (me.button != MouseEvent.LEFT_BUTTON) return 0;
1083 if (getHint(Hints.KEY_TOOL) != Hints.POINTERTOOL) return 0;
1084 assertDependencies();
1086 PickRequest req = new PickRequest(me.startCanvasPos);
1087 req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS;
1088 List<IElement> picks = new ArrayList<IElement>();
1089 pickContext.pick(diagram, req, picks);
1090 Set<IElement> sel = selection.getSelection(me.mouseId);
1092 if (Collections.disjoint(sel, picks)) return 0;
1094 return DnDConstants.ACTION_COPY;
1098 public Transferable dragStart(DragGestureEvent e) {
1100 AWTChassis chassis = (AWTChassis) e.getComponent();
1101 ICanvasContext cc = chassis.getCanvasContext();
1102 Selection sel = cc.getSingleItem(Selection.class);
1104 Set<IElement> ss = sel.getSelection(0);
1105 if (ss.isEmpty()) return null;
1106 Object[] res = new Object[ss.size()];
1108 for (IElement ee : ss)
1109 res[index++] = ee.getHint(ElementHints.KEY_OBJECT);
1111 ISelection object = new StructuredSelection(res);
1113 LocalObjectTransferable local = new LocalObjectTransferable(object);
1115 StringBuilder json = new StringBuilder();
1117 json.append(" \"type\" : \"Symbol\",");
1118 json.append(" \"res\" : [");
1120 for(int i=0;i<res.length;i++) {
1121 if(pos > 0) json.append(",");
1123 if(r instanceof IdentifiedObject) {
1124 Object id = ((IdentifiedObject) r).getId();
1125 if(id instanceof IAdaptable) {
1126 Object resource = ((IAdaptable) id).getAdapter(Resource.class);
1127 if(resource != null) {
1128 long rid = ((Resource)resource).getResourceId();
1129 json.append(Long.toString(rid));
1137 StringSelection text = new StringSelection(json.toString());
1138 PlaintextTransfer plainText = new PlaintextTransfer(json.toString());
1140 return new MultiTransferable(local, text, plainText);
1145 public int getAllowedOps() {
1146 return DnDConstants.ACTION_COPY;
1149 public void dragDropEnd(DragSourceDropEvent dsde) {
1150 // System.out.println("dragDropEnd: " + dsde);
1151 LocalObjectTransfer.getTransfer().clear();
1154 public void dragEnter(DragSourceDragEvent dsde) {
1157 public void dragExit(DragSourceEvent dse) {
1160 public void dragOver(DragSourceDragEvent dsde) {
1163 public void dropActionChanged(DragSourceDragEvent dsde) {
1167 ExpandListener groupExpandListener = new ExpandListener() {
1169 public void itemCollapsed(ExpandEvent e) {
1170 final PGroup group = (PGroup) e.widget;
1171 group.setData(KEY_USER_EXPANDED, Boolean.FALSE);
1172 storeGroupExpandedState(group, false);
1173 //System.out.println("item collapsed: " + group + ", " + sc.getClientArea());
1174 refreshScrolledComposite();
1177 public void itemExpanded(ExpandEvent e) {
1178 final PGroup group = (PGroup) e.widget;
1179 group.setData(KEY_USER_EXPANDED, Boolean.TRUE);
1180 storeGroupExpandedState(group, true);
1181 //System.out.println("item expanded: " + group + ", " + sc.getClientArea());
1182 ThreadUtils.asyncExec(swtThread, () -> {
1183 GalleryViewer viewer = initializeGroup(group);
1186 ThreadUtils.asyncExec(swtThread, () -> {
1187 if (viewer.getControl().isDisposed())
1190 refreshScrolledComposite();
1196 public boolean isDefaultExpanded() {
1197 return defaultExpanded;
1200 public void setDefaultExpanded(boolean defaultExpanded) {
1201 this.defaultExpanded = defaultExpanded;
1204 Runnable disposer(final Widget w) {
1205 return new Runnable() {
1216 * Invoke from SWT thread only.
1218 * @param targetState
1220 public void setAllExpandedStates(boolean targetState) {
1221 setDefaultExpanded(targetState);
1222 Control[] grps = c.getChildren();
1223 boolean changed = false;
1224 for (Control control : grps)
1225 changed |= setExpandedState((PGroup) control, targetState, false);
1227 refreshScrolledComposite();
1231 * Invoke from SWT thread only.
1234 * @param targetState
1237 boolean setExpandedState(PGroup grp, boolean targetState, boolean force) {
1238 if (grp.isDisposed())
1241 storeGroupExpandedState(grp, targetState);
1242 grp.setData(KEY_USER_EXPANDED, Boolean.valueOf(targetState));
1243 if ((force || grp.getExpanded() != targetState) && grp.getVisible()) {
1244 final GalleryViewer viewer = initializeGroup(grp);
1245 setGroupExpandedWithoutNotification(grp, targetState);
1246 ThreadUtils.asyncExec(swtThread, () -> {
1247 if (!grp.isDisposed()) {
1250 refreshScrolledComposite();
1258 class ImageLoader implements Runnable {
1260 private final ImageProxy imageProxy;
1261 private final ISymbolItem item;
1263 public ImageLoader(ImageProxy imageProxy, ISymbolItem item) {
1264 this.imageProxy = imageProxy;
1270 // SVG images using the SVGUniverse in SVGCache must use
1271 // AWT thread for all operations.
1272 ThreadUtils.asyncExec(AWTThread.getThreadAccess(), () -> runBlocking());
1275 private void runBlocking() {
1277 ISymbolGroup group = item.getGroup();
1279 throw new ProvisionException("No ISymbolGroup available for ISymbolItem " + item);
1281 GalleryViewer viewer = groupViewers.get(group);
1282 if (viewer == null) {
1283 // This is normal if this composite has been disposed while these are being ran.
1284 //throw new ProvisionException("No GalleryViewer available ISymbolGroup " + group);
1285 imageProxy.setSource(DefaultImages.UNKNOWN2.get());
1289 IHintContext hints = viewer.getDiagram();
1291 throw new ProvisionException("No diagram available for GalleryViewer of group " + group);
1293 hints.setHint(ISymbolItem.KEY_ELEMENT_CLASS_LISTENER, new ElementClassListener(imageCache, disposed, item));
1294 final ElementClass ec = item.getElementClass(hints);
1296 // Without this the symbol library will at times
1297 // not update the final graphics for the symbol.
1298 // It will keep displaying the hourglass pending icon instead.
1299 symbolUpdate(disposed, imageProxy, ec);
1300 } catch (ProvisionException e) {
1301 ExceptionUtils.logWarning("Failed to provide element class for symbol item " + item, e);
1302 imageProxy.setSource(DefaultImages.ERROR_DECORATOR.get());
1303 } catch (Exception e) {
1304 ExceptionUtils.logError(e);
1305 imageProxy.setSource(DefaultImages.ERROR_DECORATOR.get());
1311 static class ElementClassListener implements org.simantics.db.procedure.Listener<ElementClass> {
1312 private Map<ISymbolItem, SoftReference<ImageProxy>> imageCache;
1313 private final AtomicBoolean disposed;
1314 private final ISymbolItem item;
1316 public ElementClassListener(Map<ISymbolItem, SoftReference<ImageProxy>> imageCache, AtomicBoolean disposed, ISymbolItem item) {
1317 this.imageCache = imageCache;
1318 this.disposed = disposed;
1323 public void execute(final ElementClass ec) {
1324 //System.out.println("SYMBOL CHANGED: " + item + " - disposed=" + disposed + " - " + ec);
1326 final ImageProxy[] imageProxy = { null };
1327 SoftReference<ImageProxy> proxyRef = imageCache.get(item);
1328 if (proxyRef != null)
1329 imageProxy[0] = proxyRef.get();
1330 if (imageProxy[0] != null)
1331 scheduleSymbolUpdate(disposed, imageProxy[0], ec);
1335 public void exception(Throwable t) {
1336 Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error in ElementClass request.", t));
1340 public boolean isDisposed() {
1341 //System.out.println("ElementClassListener.isDisposed " + item + " - " + disposed.get());
1342 return disposed.get();
1346 public FilterArea getFilterArea() {
1350 public static void scheduleSymbolUpdate(final AtomicBoolean disposed, final ImageProxy imageProxy, final ElementClass ec) {
1353 ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() {
1358 symbolUpdate(disposed, imageProxy, ec);
1363 public static void symbolUpdate(final AtomicBoolean disposed, final ImageProxy imageProxy, final ElementClass ec) {
1364 StaticSymbol ss = ec.getSingleItem(StaticSymbol.class);
1365 Image source = ss == null ? DefaultImages.UNKNOWN2.get() : ss.getImage();
1366 imageProxy.setSource(source);
1369 Runnable filterActivator = new Runnable() {
1375 Listener filterActivationListener = new Listener() {
1377 public void handleEvent(Event event) {
1378 //System.out.println("event: " + event);
1379 filterActivator.run();
1383 ISymbolGroupListener groupListener = new ISymbolGroupListener() {
1385 public void itemsChanged(ISymbolGroup group) {
1386 //System.out.println("symbol group changed: " + group);
1387 GalleryViewer viewer = groupViewers.get(group);
1388 if (viewer != null) {
1389 ISymbolItem[] input = group.getItems();
1390 viewer.setInput(input);
1395 IEventHandler externalEventHandler = new IEventHandler() {
1397 public int getEventMask() {
1398 return EventTypes.AnyMask;
1401 public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) {
1402 IEventHandler handler = SymbolLibraryComposite.this.eventHandler;
1403 return handler != null && EventTypes.passes(handler, e) ? handler.handleEvent(e) : false;
1407 protected volatile IEventHandler eventHandler;
1410 * @param eventHandler
1412 public void setEventHandler(IEventHandler eventHandler) {
1413 this.eventHandler = eventHandler;
1416 protected void storeGroupExpandedState(PGroup group, boolean expanded) {
1417 ISymbolGroup symbolGroup = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP);
1418 //System.out.println("setGroupExpandedWithoutNotification(" + group + ", " + expanded + ", " + symbolGroup + ")");
1419 if (symbolGroup != null) {
1420 Object key = symbolGroupToKey(symbolGroup);
1421 expandedGroups.put(key, expanded ? Boolean.TRUE : Boolean.FALSE);
1425 private static Object symbolGroupToKey(ISymbolGroup symbolGroup) {
1426 if (symbolGroup instanceof IIdentifiedObject)
1427 return ((IIdentifiedObject) symbolGroup).getId();
1428 return new Tuple2(symbolGroup.getName(), symbolGroup.getDescription());