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.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;
145 * @author Tuukka Lehtonen
147 public class SymbolLibraryComposite extends Composite {
149 private static final int FILTER_DELAY = 500;
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";
155 /** Root composite */
156 ScrolledComposite sc;
158 IThreadWorkQueue swtThread;
159 boolean defaultExpanded = false;
160 ISymbolProvider symbolProvider;
161 AtomicBoolean disposed = new AtomicBoolean(false);
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.
170 AtomicInteger loadCount = new AtomicInteger();
172 Map<ISymbolGroup, PGroup> groups = new HashMap<>();
173 Map<ISymbolGroup, GalleryViewer> groupViewers = new HashMap<>();
174 Map<Object, Boolean> expandedGroups = new HashMap<>();
175 LocalResourceManager resourceManager;
178 ThreadFactory threadFactory = new ThreadFactory() {
180 public Thread newThread(Runnable r) {
181 Thread t = new Thread(r, "Symbol Library Loader");
183 t.setPriority(Thread.NORM_PRIORITY);
188 Semaphore loaderSemaphore = new Semaphore(1);
189 ExecutorService loaderExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
190 2L, TimeUnit.SECONDS,
191 new SynchronousQueue<Runnable>(),
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)}.
199 Map<ISymbolItem, SoftReference<ImageProxy>> imageCache = new WeakHashMap<ISymbolItem, SoftReference<ImageProxy>>();
201 static final Pattern ANY = Pattern.compile(".*");
202 Pattern currentFilterPattern = ANY;
204 FilterConfiguration config = new FilterConfiguration();
205 IFilter currentGroupFilter = AcceptAllFilter.getInstance();
207 ErrorHandler errorHandler = LogErrorHandler.INSTANCE;
209 static class GroupDescriptor {
210 public final ISymbolGroup lib;
211 public final String label;
212 public final String description;
213 public final PGroup group;
215 public GroupDescriptor(ISymbolGroup lib, String label, String description, PGroup group) {
217 assert(label != null);
220 this.description = description;
225 Comparator<GroupDescriptor> groupComparator = new Comparator<GroupDescriptor>() {
227 public int compare(GroupDescriptor o1, GroupDescriptor o2) {
228 return o1.label.compareToIgnoreCase(o2.label);
232 static final EnumSet<Feature> VOLATILE = EnumSet.of(Feature.Volatile);
234 static class PendingImage extends ImageProxy {
235 EnumSet<Feature> features;
236 PendingImage(Image source, EnumSet<Feature> features) {
238 this.features = features;
241 public EnumSet<Feature> getFeatures() {
246 class LabelProvider extends BaseLabelProvider implements ILabelProvider {
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();
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);
263 public String getText(final Object element) {
264 return ((ISymbolItem) element).getName();
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;
275 public java.awt.Image getToolTipImage(Object object) {
279 public Color getToolTipBackgroundColor(Object object) {
284 public Color getToolTipForegroundColor(Object object) {
289 public SymbolLibraryComposite(final Composite parent, int style, SymbolProviderFactory symbolProvider) {
290 super(parent, style);
292 Simantics.getSession().asyncRequest(new CreateSymbolProvider(symbolProvider), new SymbolProviderListener());
293 addDisposeListener(new DisposeListener() {
295 public void widgetDisposed(DisposeEvent e) {
304 static class CreateSymbolProvider extends UnaryRead<SymbolProviderFactory, ISymbolProvider> {
305 public CreateSymbolProvider(SymbolProviderFactory factory) {
309 public ISymbolProvider perform(ReadGraph graph) throws DatabaseException {
310 //System.out.println("CreateSymbolProvider.perform: " + parameter);
311 ISymbolProvider provider = parameter.create(graph);
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);
333 class SymbolProviderListener extends ListenerAdapter<ISymbolProvider> {
335 public void exception(Throwable t) {
336 ErrorLogger.defaultLogError(t);
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();
348 public boolean isDisposed() {
349 boolean result = SymbolLibraryComposite.this.isDisposed();
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));
358 this.resourceManager = new LocalResourceManager(JFaceResources.getResources(getDisplay()), this);
359 swtThread = SWTThread.getThreadAccess(this);
361 filter = new FilterArea(this, SWT.NONE);
362 GridDataFactory.fillDefaults().grab(true, false).applyTo(filter);
363 filter.getText().addModifyListener(new ModifyListener() {
365 //long lastModificationTime = -1000;
367 public void modifyText(ModifyEvent e) {
368 scheduleDelayedFilter(FILTER_DELAY, TimeUnit.MILLISECONDS);
370 private void scheduleDelayedFilter(long filterDelay, TimeUnit delayUnit) {
371 final String text = filter.getText().getText();
373 //long time = System.currentTimeMillis();
374 //long delta = time - lastModificationTime;
375 //lastModificationTime = time;
377 final int count = ++modCount;
378 ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
381 int newCount = modCount;
382 if (newCount != count)
385 ThreadUtils.asyncExec(swtThread, new Runnable() {
390 if (!filterGroups(text)) {
391 scheduleDelayedFilter(100, TimeUnit.MILLISECONDS);
396 }, filterDelay, delayUnit);
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() {
409 public void controlResized(ControlEvent e) {
410 //System.out.println("ScrolledComposite resized: " + sc.getSize());
411 refreshScrolledComposite();
414 //sc.setBackground(sc.getDisplay().getSystemColor(SWT.COLOR_RED));
416 c = new Composite(sc, 0);
418 GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(c);
419 //c.setBackground(c.getDisplay().getSystemColor(SWT.COLOR_BLUE));
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);
428 c.addDisposeListener(new DisposeListener() {
430 public void widgetDisposed(DisposeEvent e) {
431 // Remember to shutdown the executor
432 loaderExecutor.shutdown();
433 groupViewers.clear();
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() {
448 syncRefreshScrolledComposite();
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);
465 * (Re-)Load symbol groups, refresh the content
467 void load(Collection<ISymbolGroup> _libraries) {
468 if (_libraries == null)
469 _libraries = Collections.emptyList();
470 final Collection<ISymbolGroup> libraries = _libraries;
471 if (loaderExecutor.isShutdown())
473 loaderExecutor.execute(new Runnable() {
476 // Increment loadCount to signal that a new load cycle is on the way.
477 Integer loadId = loadCount.incrementAndGet();
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);
487 loaderSemaphore.release();
488 ExceptionUtils.logAndShowError(e);
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();
500 groupViewers.remove(entry.getKey());
501 if (group != null && !group.isDisposed())
502 ThreadUtils.asyncExec(swtThread, disposer(group));
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));
514 // Populate all the missing groups.
515 IFilter groupFilter = currentGroupFilter;
519 groupDescs.iterator(),
525 loaderSemaphore.release();
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)
541 // Check whether to still continue this population or not.
542 int currentLoadId = loadCount.get();
543 if (currentLoadId != loadId) {
548 if (!iter.hasNext()) {
549 ThreadUtils.asyncExec(swtThread, new Runnable() {
552 if (filter.isDisposed() || c.isDisposed())
562 final GroupDescriptor desc = iter.next();
564 ThreadUtils.asyncExec(swtThread, new Runnable() {
567 // Must make sure that loadComplete is invoked under error
571 } catch (RuntimeException e) {
573 ExceptionUtils.logAndShowError(e);
576 ExceptionUtils.logAndShowError(e);
580 public void populateGroup() {
581 if (c.isDisposed()) {
586 //System.out.println("populating: " + desc.label);
587 PGroup group = desc.group;
588 Runnable chainedCompletionCallback = loadComplete;
589 if (group == null || group.isDisposed()) {
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);
603 group.moveAbove(null);
606 installMouseEventAdapter(group, new SWTMouseEventAdapter(group, externalEventHandler));
608 groups.put(desc.lib, group);
610 Boolean shouldBeExpanded = expandedGroups.get(symbolGroupToKey(desc.lib));
611 if (shouldBeExpanded == null)
612 shouldBeExpanded = defaultExpanded;
613 group.setData(KEY_USER_EXPANDED, shouldBeExpanded);
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);
621 // Track group content changes if possible.
622 if (desc.lib instanceof IModifiableSymbolGroup) {
623 IModifiableSymbolGroup mod = (IModifiableSymbolGroup) desc.lib;
624 mod.addListener(groupListener);
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));
638 group.setData(SymbolLibraryKeys.KEY_GROUP, desc.lib);
639 group.setText(desc.label);
640 group.setToolTipText(desc.description);
642 // Hide the group immediately if necessary.
643 boolean groupFiltered = !groupFilter.select(desc.label);
644 group.setData(KEY_GROUP_FILTERED, Boolean.valueOf(groupFiltered));
646 setGroupVisible(group, false);
648 syncRefreshScrolledComposite();
650 final PGroup group_ = group;
651 Runnable newCompletionCallback = chainedCompletionCallback;
653 populateGroups(exec, group_, iter, groupFilter, loadId, newCompletionCallback);
659 protected void installMouseEventAdapter(Control onControl, SWTMouseEventAdapter eventAdapter) {
660 onControl.addMouseListener(eventAdapter);
661 onControl.addMouseTrackListener(eventAdapter);
662 onControl.addMouseMoveListener(eventAdapter);
663 onControl.addMouseWheelListener(eventAdapter);
668 * @return <code>null</code> if GalleryViewer is currently being created
670 GalleryViewer initializeGroup(final PGroup group) {
671 if (group.isDisposed())
674 //System.out.println("initializeGroup(" + group.getText() + ")");
676 synchronized (group) {
677 if (group.getData(KEY_VIEWER_INITIALIZED) != null) {
678 return (GalleryViewer) group.getData(SymbolLibraryKeys.KEY_GALLERY);
680 group.setData(KEY_VIEWER_INITIALIZED, Boolean.TRUE);
683 //System.out.println("initializing group: " + group.getText());
685 // NOTE: this will NOT stop to wait until the SWT/AWT UI
686 // population has been completed.
687 GalleryViewer viewer = new GalleryViewer(group);
689 ISymbolGroup input = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP);
690 initializeViewer(group, input, viewer);
692 groupViewers.put(input, viewer);
693 group.setData(SymbolLibraryKeys.KEY_GALLERY, viewer);
695 //System.out.println("initialized group: " + group.getText());
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);
706 viewer.setContentProvider(new IStructuredContentProvider() {
709 * Returns the elements in the input, which must be either an array or a
710 * <code>Collection</code>.
713 public Object[] getElements(Object inputElement) {
714 if(inputElement == null) return new Object[0];
715 return ((ISymbolGroup)inputElement).getItems();
719 * This implementation does nothing.
722 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
727 * This implementation does nothing.
730 public void dispose() {
735 viewer.setLabelProvider(new LabelProvider());
736 viewer.setInput(input);
738 // Add event handler that closes libraries on double clicks into empty
740 viewer.getCanvasContext().getEventHandlerStack().add(new IEventHandler() {
742 public int getEventMask() {
743 return EventTypes.MouseDoubleClickMask;
747 public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) {
748 if (externalEventHandler.handleEvent(e))
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())
758 //System.out.println("NOTHING CLICKED");
759 if (group.isDisposed())
761 group.getDisplay().asyncExec(() -> {
762 if (group.isDisposed())
765 boolean exp = !group.getExpanded();
766 group.setData(KEY_USER_EXPANDED, Boolean.valueOf(exp));
767 setGroupExpandedWithoutNotification(group, exp);
768 refreshScrolledComposite();
777 static String toPatternString(String filter) {
778 return DefaultFilterStrategy.defaultToPatternString(filter, true);
781 static class SymbolItemFilter extends ViewerFilter {
782 private final String string;
783 private final Matcher m;
785 public SymbolItemFilter(String string, Pattern pattern) {
786 this.string = string;
787 this.m = pattern.matcher("");
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());
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"));
810 public int hashCode() {
811 return string == null ? 0 : string.hashCode();
815 public boolean equals(Object obj) {
820 if (getClass() != obj.getClass())
822 SymbolItemFilter other = (SymbolItemFilter) obj;
823 if (string == null) {
824 if (other.string != null)
826 } else if (!string.equals(other.string))
832 static Pattern toPattern(String filterText) {
833 String regExFilter = toPatternString(filterText);
834 Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY;
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()) {
843 patterns.add(toPattern(f.getFilterText()));
845 return new IFilter() {
847 public boolean select(Object toTest) {
848 if (patterns.isEmpty())
851 String s = (String) toTest;
854 for (Pattern pat : patterns) {
855 Matcher m = pat.matcher(s.toLowerCase());
856 //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL"));
862 for (Pattern pat : patterns) {
863 Matcher m = pat.matcher(s.toLowerCase());
864 //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL"));
870 throw new Error("Shouldn't happen");
876 void updateFilterConfiguration(FilterConfiguration config) {
877 this.config = config;
878 IFilter filter = composeFilter(config);
879 this.currentGroupFilter = filter;
882 void applyGroupFilters() {
883 IFilter groupFilter = this.currentGroupFilter;
884 final boolean[] changed = new boolean[] { false };
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;
894 grp.setData(KEY_GROUP_FILTERED, Boolean.valueOf(!shouldBeVisible));
896 setGroupVisible(grp, shouldBeVisible);
900 ThreadUtils.asyncExec(swtThread, new Runnable() {
907 syncRefreshScrolledComposite();
914 * Filters the symbol groups and makes them visible/invisible as necessary.
915 * Invoke only from the SWT thread.
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
921 boolean filterGroups(String text) {
922 //System.out.println("FILTERING WITH TEXT: " + text);
924 String regExFilter = toPatternString(text);
925 Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY;
927 this.currentFilterPattern = pattern;
928 final boolean[] changed = new boolean[] { false };
929 boolean filteringComplete = true;
931 ViewerFilter filter = null;
932 if (regExFilter != null)
933 filter = new SymbolItemFilter(regExFilter, pattern);
935 Control[] grps = c.getChildren();
936 for (Control ctrl : grps) {
937 final PGroup grp = (PGroup) ctrl;
938 if (grp.isDisposed())
940 Boolean contentsChanged = filterGroup(grp, filter);
941 if (contentsChanged == null)
942 filteringComplete = false;
944 changed[0] = contentsChanged;
947 ThreadUtils.asyncExec(swtThread, new Runnable() {
954 syncRefreshScrolledComposite();
959 return filteringComplete;
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);
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
976 private Boolean filterGroup(PGroup grp, ViewerFilter filter) {
977 boolean changed = false;
978 GalleryViewer viewer = initializeGroup(grp);
982 ViewerFilter lastFilter = viewer.getFilter();
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);
992 // Find out how much data would be shown with the new filter.
993 viewer.setFilter(filterMatchesGroup ? null : filter);
994 Object[] elements = viewer.getFilteredElements();
996 boolean shouldBeVisible = !groupFiltered && (elements.length > 0 || filterMatchesGroup);
997 boolean shouldBeExpanded = shouldBeVisible && (filter != null || userExpanded);
999 // System.out.format("%40s: filterMatchesGroup(%s) = %s, visible/should be = %5s %5s, expanded/user expanded/should be = %5s %5s %5s\n",
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));
1009 if (filterChanged || visible != shouldBeVisible || expanded != shouldBeExpanded) {
1012 if (shouldBeVisible == userExpanded) {
1013 if (expanded != shouldBeExpanded)
1014 setGroupExpandedWithoutNotification(grp, shouldBeExpanded);
1015 setGroupVisible(grp, shouldBeVisible);
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);
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);
1030 // All groups should be visible. Some should be expanded and others not.
1031 if (expanded != userExpanded)
1032 setGroupExpandedWithoutNotification(grp, userExpanded);
1034 setGroupVisible(grp, true);
1038 if (shouldBeExpanded) {
1039 viewer.refreshWithContent(elements);
1043 // String label = grp.getText();
1044 // Matcher m = pattern.matcher(label.toLowerCase());
1045 // boolean visible = m.matches();
1046 // if (visible != grp.getVisible()) {
1048 // setGroupVisible(grp, visible);
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);
1063 void setGroupVisible(PGroup group, boolean visible) {
1064 GridData gd = (GridData) group.getLayoutData();
1065 gd.exclude = !visible;
1066 group.setVisible(visible);
1069 boolean isGroupFiltered(String label) {
1070 return !currentFilterPattern.matcher(label.toLowerCase()).matches();
1073 class DragSourceParticipant extends AbstractDiagramParticipant implements IDragSourceParticipant {
1074 @Reference Selection selection;
1075 @Dependency PointerInteractor pi;
1076 @Dependency TransformUtil util;
1077 @Dependency PickContext pickContext;
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();
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);
1091 if (Collections.disjoint(sel, picks)) return 0;
1093 return DnDConstants.ACTION_COPY;
1097 public Transferable dragStart(DragGestureEvent e) {
1099 AWTChassis chassis = (AWTChassis) e.getComponent();
1100 ICanvasContext cc = chassis.getCanvasContext();
1101 Selection sel = cc.getSingleItem(Selection.class);
1103 Set<IElement> ss = sel.getSelection(0);
1104 if (ss.isEmpty()) return null;
1105 Object[] res = new Object[ss.size()];
1107 for (IElement ee : ss)
1108 res[index++] = ee.getHint(ElementHints.KEY_OBJECT);
1110 ISelection object = new StructuredSelection(res);
1112 LocalObjectTransferable local = new LocalObjectTransferable(object);
1114 StringBuilder json = new StringBuilder();
1116 json.append(" \"type\" : \"Symbol\",");
1117 json.append(" \"res\" : [");
1119 for(int i=0;i<res.length;i++) {
1120 if(pos > 0) json.append(",");
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));
1133 String jsonText = json.toString();
1134 StringSelection text = new StringSelection(jsonText);
1135 PlaintextTransfer plainText = new PlaintextTransfer(jsonText);
1137 return new MultiTransferable(local, text, plainText);
1142 public int getAllowedOps() {
1143 return DnDConstants.ACTION_COPY;
1146 public void dragDropEnd(DragSourceDropEvent dsde) {
1147 // System.out.println("dragDropEnd: " + dsde);
1148 LocalObjectTransfer.getTransfer().clear();
1151 public void dragEnter(DragSourceDragEvent dsde) {
1154 public void dragExit(DragSourceEvent dse) {
1157 public void dragOver(DragSourceDragEvent dsde) {
1160 public void dropActionChanged(DragSourceDragEvent dsde) {
1164 ExpandListener groupExpandListener = new ExpandListener() {
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();
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);
1183 ThreadUtils.asyncExec(swtThread, () -> {
1184 if (viewer.getControl().isDisposed())
1187 refreshScrolledComposite();
1193 public boolean isDefaultExpanded() {
1194 return defaultExpanded;
1197 public void setDefaultExpanded(boolean defaultExpanded) {
1198 this.defaultExpanded = defaultExpanded;
1201 Runnable disposer(final Widget w) {
1202 return new Runnable() {
1213 * Invoke from SWT thread only.
1215 * @param targetState
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);
1224 refreshScrolledComposite();
1228 * Invoke from SWT thread only.
1231 * @param targetState
1234 boolean setExpandedState(PGroup grp, boolean targetState, boolean force) {
1235 if (grp.isDisposed())
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()) {
1247 refreshScrolledComposite();
1255 class ImageLoader implements Runnable {
1257 private final ImageProxy imageProxy;
1258 private final ISymbolItem item;
1260 public ImageLoader(ImageProxy imageProxy, ISymbolItem item) {
1261 this.imageProxy = imageProxy;
1267 // SVG images using the SVGUniverse in SVGCache must use
1268 // AWT thread for all operations.
1269 ThreadUtils.asyncExec(AWTThread.getThreadAccess(), () -> runBlocking());
1272 private void runBlocking() {
1274 ISymbolGroup group = item.getGroup();
1276 throw new ProvisionException("No ISymbolGroup available for ISymbolItem " + item);
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());
1286 IHintContext hints = viewer.getDiagram();
1288 throw new ProvisionException("No diagram available for GalleryViewer of group " + group);
1290 hints.setHint(ISymbolItem.KEY_ELEMENT_CLASS_LISTENER, new ElementClassListener(imageCache, disposed, item));
1291 final ElementClass ec = item.getElementClass(hints);
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());
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;
1313 public ElementClassListener(Map<ISymbolItem, SoftReference<ImageProxy>> imageCache, AtomicBoolean disposed, ISymbolItem item) {
1314 this.imageCache = imageCache;
1315 this.disposed = disposed;
1320 public void execute(final ElementClass ec) {
1321 //System.out.println("SYMBOL CHANGED: " + item + " - disposed=" + disposed + " - " + ec);
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);
1332 public void exception(Throwable t) {
1333 Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error in ElementClass request.", t));
1337 public boolean isDisposed() {
1338 //System.out.println("ElementClassListener.isDisposed " + item + " - " + disposed.get());
1339 return disposed.get();
1343 public FilterArea getFilterArea() {
1347 public static void scheduleSymbolUpdate(final AtomicBoolean disposed, final ImageProxy imageProxy, final ElementClass ec) {
1350 ThreadUtils.asyncExec(AWTThread.getThreadAccess(), new Runnable() {
1355 symbolUpdate(disposed, imageProxy, ec);
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);
1366 Runnable filterActivator = new Runnable() {
1372 Listener filterActivationListener = new Listener() {
1374 public void handleEvent(Event event) {
1375 //System.out.println("event: " + event);
1376 filterActivator.run();
1380 ISymbolGroupListener groupListener = new ISymbolGroupListener() {
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);
1392 IEventHandler externalEventHandler = new IEventHandler() {
1394 public int getEventMask() {
1395 return EventTypes.AnyMask;
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;
1404 protected volatile IEventHandler eventHandler;
1407 * @param eventHandler
1409 public void setEventHandler(IEventHandler eventHandler) {
1410 this.eventHandler = eventHandler;
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);
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());