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.Transferable;
15 import java.awt.dnd.DnDConstants;
16 import java.awt.dnd.DragGestureEvent;
17 import java.awt.dnd.DragSourceDragEvent;
18 import java.awt.dnd.DragSourceDropEvent;
19 import java.awt.dnd.DragSourceEvent;
20 import java.lang.ref.SoftReference;
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.Comparator;
25 import java.util.EnumSet;
26 import java.util.HashMap;
27 import java.util.Iterator;
28 import java.util.List;
31 import java.util.TreeSet;
32 import java.util.WeakHashMap;
33 import java.util.concurrent.ExecutorService;
34 import java.util.concurrent.Executors;
35 import java.util.concurrent.Semaphore;
36 import java.util.concurrent.ThreadFactory;
37 import java.util.concurrent.TimeUnit;
38 import java.util.concurrent.atomic.AtomicInteger;
39 import java.util.regex.Matcher;
40 import java.util.regex.Pattern;
42 import org.eclipse.core.runtime.preferences.IEclipsePreferences;
43 import org.eclipse.core.runtime.preferences.InstanceScope;
44 import org.eclipse.jface.action.Action;
45 import org.eclipse.jface.action.IToolBarManager;
46 import org.eclipse.jface.layout.GridDataFactory;
47 import org.eclipse.jface.layout.GridLayoutFactory;
48 import org.eclipse.jface.resource.FontDescriptor;
49 import org.eclipse.jface.resource.JFaceResources;
50 import org.eclipse.jface.resource.LocalResourceManager;
51 import org.eclipse.jface.viewers.AcceptAllFilter;
52 import org.eclipse.jface.viewers.BaseLabelProvider;
53 import org.eclipse.jface.viewers.IFilter;
54 import org.eclipse.jface.viewers.ISelection;
55 import org.eclipse.jface.viewers.IStructuredContentProvider;
56 import org.eclipse.jface.viewers.StructuredSelection;
57 import org.eclipse.jface.viewers.Viewer;
58 import org.eclipse.jface.viewers.ViewerFilter;
59 import org.eclipse.jface.window.Window;
60 import org.eclipse.nebula.widgets.pgroup.PGroup;
61 import org.eclipse.swt.SWT;
62 import org.eclipse.swt.custom.ScrolledComposite;
63 import org.eclipse.swt.events.ControlAdapter;
64 import org.eclipse.swt.events.ControlEvent;
65 import org.eclipse.swt.events.DisposeEvent;
66 import org.eclipse.swt.events.DisposeListener;
67 import org.eclipse.swt.events.ExpandEvent;
68 import org.eclipse.swt.events.ExpandListener;
69 import org.eclipse.swt.events.ModifyEvent;
70 import org.eclipse.swt.events.ModifyListener;
71 import org.eclipse.swt.graphics.Color;
72 import org.eclipse.swt.graphics.Point;
73 import org.eclipse.swt.graphics.Rectangle;
74 import org.eclipse.swt.layout.GridData;
75 import org.eclipse.swt.widgets.Composite;
76 import org.eclipse.swt.widgets.Control;
77 import org.eclipse.swt.widgets.Event;
78 import org.eclipse.swt.widgets.Listener;
79 import org.eclipse.swt.widgets.Widget;
80 import org.eclipse.ui.IMemento;
81 import org.eclipse.ui.contexts.IContextService;
82 import org.eclipse.ui.part.ViewPart;
83 import org.osgi.service.prefs.BackingStoreException;
84 import org.simantics.db.management.ISessionContextChangedListener;
85 import org.simantics.db.management.ISessionContextProvider;
86 import org.simantics.db.management.SessionContextChangedEvent;
87 import org.simantics.diagram.internal.Activator;
88 import org.simantics.diagram.symbollibrary.IModifiableSymbolGroup;
89 import org.simantics.diagram.symbollibrary.ISymbolGroup;
90 import org.simantics.diagram.symbollibrary.ISymbolGroupListener;
91 import org.simantics.diagram.symbollibrary.ISymbolItem;
92 import org.simantics.diagram.symbollibrary.ISymbolManager;
93 import org.simantics.diagram.symbollibrary.ui.FilterConfiguration.Mode;
94 import org.simantics.diagram.synchronization.ErrorHandler;
95 import org.simantics.diagram.synchronization.LogErrorHandler;
96 import org.simantics.diagram.synchronization.SynchronizationHints;
97 import org.simantics.g2d.canvas.Hints;
98 import org.simantics.g2d.canvas.ICanvasContext;
99 import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
100 import org.simantics.g2d.canvas.impl.DependencyReflection.Reference;
101 import org.simantics.g2d.chassis.AWTChassis;
102 import org.simantics.g2d.diagram.DiagramUtils;
103 import org.simantics.g2d.diagram.handler.PickContext;
104 import org.simantics.g2d.diagram.handler.PickRequest;
105 import org.simantics.g2d.diagram.handler.layout.FlowLayout;
106 import org.simantics.g2d.diagram.participant.AbstractDiagramParticipant;
107 import org.simantics.g2d.diagram.participant.Selection;
108 import org.simantics.g2d.diagram.participant.pointertool.PointerInteractor;
109 import org.simantics.g2d.dnd.IDragSourceParticipant;
110 import org.simantics.g2d.element.ElementClass;
111 import org.simantics.g2d.element.ElementHints;
112 import org.simantics.g2d.element.IElement;
113 import org.simantics.g2d.element.handler.StaticSymbol;
114 import org.simantics.g2d.gallery.GalleryViewer;
115 import org.simantics.g2d.gallery.ILabelProvider;
116 import org.simantics.g2d.image.DefaultImages;
117 import org.simantics.g2d.image.Image;
118 import org.simantics.g2d.image.Image.Feature;
119 import org.simantics.g2d.image.impl.ImageProxy;
120 import org.simantics.g2d.participant.TransformUtil;
121 import org.simantics.project.IProject;
122 import org.simantics.project.ProjectKeys;
123 import org.simantics.scenegraph.INode;
124 import org.simantics.scenegraph.g2d.events.EventTypes;
125 import org.simantics.scenegraph.g2d.events.IEventHandler;
126 import org.simantics.scenegraph.g2d.events.MouseEvent;
127 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent;
128 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
129 import org.simantics.ui.SimanticsUI;
130 import org.simantics.ui.dnd.LocalObjectTransfer;
131 import org.simantics.ui.dnd.LocalObjectTransferable;
132 import org.simantics.utils.datastructures.cache.ProvisionException;
133 import org.simantics.utils.datastructures.hints.HintListenerAdapter;
134 import org.simantics.utils.datastructures.hints.HintTracker;
135 import org.simantics.utils.datastructures.hints.IHintContext.Key;
136 import org.simantics.utils.datastructures.hints.IHintListener;
137 import org.simantics.utils.datastructures.hints.IHintObservable;
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.BundleUtils;
142 import org.simantics.utils.ui.ExceptionUtils;
143 import org.simantics.utils.ui.workbench.StringMemento;
146 * @author Tuukka Lehtonen
148 public class SymbolLibraryView extends ViewPart {
150 private static final String SYMBOL_LIBRARY_CONTEXT = "org.simantics.diagram.symbolLibrary";
151 private static final String PREF_FILTERS = "filters";
152 private static final String TAG_FILTER_MODE = "filterMode";
153 private static final String TAG_FILTER = "filter";
155 private static final String ATTR_ACTIVE = "active";
156 private static final String ATTR_FILTER_TEXT = "filterText";
157 private static final String ATTR_NAME = "name";
159 private static final int FILTER_DELAY = 500;
161 private static final String KEY_VIEWER_INITIALIZED = "viewer.initialized";
162 private static final String KEY_USER_EXPANDED = "userExpanded";
163 private static final String KEY_GROUP_FILTERED = "groupFiltered";
165 /** Root composite */
166 ScrolledComposite sc;
168 ISessionContextProvider sessionCtxProvider;
169 IThreadWorkQueue swtThread;
170 boolean defaultExpanded = false;
173 * This value is incremented each time a load method is called and symbol
174 * group population is started. It can be used by
175 * {@link #populateGroups(ExecutorService, Control, Iterator, IFilter)} to
176 * tell whether it should stop its population job because a later load
177 * will override its results anyway.
179 AtomicInteger loadCount = new AtomicInteger();
181 Map<ISymbolGroup, PGroup> groups = new HashMap<ISymbolGroup, PGroup>();
182 Map<ISymbolGroup, GalleryViewer> groupViewers = new HashMap<ISymbolGroup, GalleryViewer>();
183 LocalResourceManager resourceManager;
186 ThreadFactory threadFactory = new ThreadFactory() {
188 public Thread newThread(Runnable r) {
189 Thread t = new Thread(r, "Symbol Library Loader");
191 t.setPriority(Thread.NORM_PRIORITY);
196 Semaphore loaderSemaphore = new Semaphore(1);
197 ExecutorService loaderExecutor = Executors.newCachedThreadPool(threadFactory);
200 * Used to prevent annoying reloading of symbols when groups are closed and
201 * reopened by not always having to schedule an {@link ImageLoader} in
202 * {@link LabelProvider#getImage(Object)}.
204 Map<ISymbolItem, SoftReference<ImageProxy>> imageCache = new WeakHashMap<ISymbolItem, SoftReference<ImageProxy>>();
206 static final Pattern ANY = Pattern.compile(".*");
207 Pattern currentFilterPattern = ANY;
209 FilterConfiguration config = new FilterConfiguration();
210 IFilter currentGroupFilter = AcceptAllFilter.getInstance();
212 ErrorHandler errorHandler = LogErrorHandler.INSTANCE;
214 static class GroupDescriptor {
215 public final ISymbolGroup lib;
216 public final String label;
217 public final PGroup group;
219 public GroupDescriptor(ISymbolGroup lib, String label, PGroup group) {
221 assert(label != null);
228 Comparator<GroupDescriptor> groupComparator = new Comparator<GroupDescriptor>() {
230 public int compare(GroupDescriptor o1, GroupDescriptor o2) {
231 return o1.label.compareToIgnoreCase(o2.label);
235 static final EnumSet<Feature> VOLATILE = EnumSet.of(Feature.Volatile);
237 static class PendingImage extends ImageProxy {
238 EnumSet<Feature> features;
239 PendingImage(Image source, EnumSet<Feature> features) {
241 this.features = features;
244 public EnumSet<Feature> getFeatures() {
249 class LabelProvider extends BaseLabelProvider implements ILabelProvider {
251 public Image getImage(final Object element) {
252 ISymbolItem item = (ISymbolItem) element;
253 // Use a volatile ImageProxy to make the image loading asynchronous.
254 ImageProxy proxy = null;
255 SoftReference<ImageProxy> proxyRef = imageCache.get(item);
256 if (proxyRef != null)
257 proxy = proxyRef.get();
259 proxy = new PendingImage(DefaultImages.HOURGLASS.get(), VOLATILE);
260 imageCache.put(item, new SoftReference<ImageProxy>(proxy));
261 ThreadUtils.getNonBlockingWorkExecutor().schedule(new ImageLoader(proxy, item), 100, TimeUnit.MILLISECONDS);
266 public String getText(final Object element) {
267 return ((ISymbolItem) element).getName();
270 public String getToolTipText(Object element) {
271 ISymbolItem item = (ISymbolItem) element;
272 String name = item.getName();
273 String desc = item.getDescription();
274 return name.equals(desc) ? name : name + " - " + desc;
278 public java.awt.Image getToolTipImage(Object object) {
282 public Color getToolTipBackgroundColor(Object object) {
287 public Color getToolTipForegroundColor(Object object) {
292 public class ProjectTracker extends HintTracker {
293 public ProjectTracker() {
294 IHintListener symbolGroupListener = new HintListenerAdapter() {
296 public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
297 @SuppressWarnings("unchecked")
298 Collection<ISymbolGroup> groups = (Collection<ISymbolGroup>) newValue;
302 addKeyHintListener(ISymbolManager.KEY_SYMBOL_GROUPS, symbolGroupListener);
305 ProjectTracker projectTracker= new ProjectTracker();
307 public class SessionContextTracker extends HintTracker implements ISessionContextChangedListener {
308 public SessionContextTracker() {
309 IHintListener activeProjectListener = new HintListenerAdapter() {
311 public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
312 projectTracker.track((IProject) newValue);
315 addKeyHintListener(ProjectKeys.KEY_PROJECT, activeProjectListener);
318 public void sessionContextChanged(SessionContextChangedEvent event) {
319 track(event.getNewValue());
322 SessionContextTracker sessionContextTracker = new SessionContextTracker();
324 void attachToSession() {
325 // Track active ISessionContext changes
326 sessionCtxProvider = SimanticsUI.getSessionContextProvider(getViewSite().getWorkbenchWindow());
327 sessionCtxProvider.addContextChangedListener(sessionContextTracker);
329 // Start tracking the current session context for input changes.
330 // This will/must cause applySessionContext to get called.
331 // Doing the applySessionContext initialization this way
332 // instead of directly calling it will also make sure that
333 // applySessionContext is only called once when first initialized,
334 // and not twice like with the direct invocation.
335 sessionContextTracker.track(sessionCtxProvider.getSessionContext());
338 public void readPreferences() {
339 FilterConfiguration config = new FilterConfiguration();
341 IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(Activator.PLUGIN_ID);
342 String filters = prefs.get(PREF_FILTERS, null);
344 if (filters != null) {
345 IMemento memento = new StringMemento(filters);
346 for (IMemento child : memento.getChildren(TAG_FILTER)) {
347 String name = child.getString(ATTR_NAME);
348 String filterText = child.getString(ATTR_FILTER_TEXT);
349 boolean active = Boolean.TRUE.equals(child.getBoolean(ATTR_ACTIVE));
351 if (name != null && !name.trim().isEmpty() && filterText != null && !filterText.isEmpty()) {
352 config.getFilters().add(new GroupFilter(name, filterText, active));
355 Collections.sort(config.getFilters());
357 String filterMode = memento.getString(TAG_FILTER_MODE);
358 if (filterMode != null) {
360 Mode mode = Mode.valueOf(filterMode);
361 config.setMode(mode);
362 } catch (IllegalArgumentException e) {
367 updateFilterConfiguration(config);
370 void savePreferences() throws BackingStoreException {
371 IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(Activator.PLUGIN_ID);
373 IMemento memento = new StringMemento();
374 for (GroupFilter f : config.getFilters()) {
375 IMemento child = memento.createChild(TAG_FILTER);
376 child.putString(ATTR_NAME, f.getName());
377 child.putString(ATTR_FILTER_TEXT, f.getFilterText());
378 child.putBoolean(ATTR_ACTIVE, f.isActive());
380 memento.putString(TAG_FILTER_MODE, config.getMode().toString());
382 prefs.put(PREF_FILTERS, memento.toString());
388 public void createPartControl(final Composite parent) {
389 // Prime the image, make it available.
390 DefaultImages.HOURGLASS.get();
394 this.resourceManager = new LocalResourceManager(JFaceResources.getResources(parent.getDisplay()), parent);
395 swtThread = SWTThread.getThreadAccess(parent);
397 GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(parent);
399 filter = new FilterArea(parent, SWT.NONE);
400 GridDataFactory.fillDefaults().grab(true, false).applyTo(filter);
401 filter.getText().addModifyListener(new ModifyListener() {
403 //long lastModificationTime = -1000;
405 public void modifyText(ModifyEvent e) {
406 scheduleDelayedFilter(FILTER_DELAY, TimeUnit.MILLISECONDS);
408 private void scheduleDelayedFilter(long filterDelay, TimeUnit delayUnit) {
409 final String text = filter.getText().getText();
411 //long time = System.currentTimeMillis();
412 //long delta = time - lastModificationTime;
413 //lastModificationTime = time;
415 final int count = ++modCount;
416 ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
419 int newCount = modCount;
420 if (newCount != count)
423 ThreadUtils.asyncExec(swtThread, new Runnable() {
428 if (!filterGroups(text)) {
429 scheduleDelayedFilter(100, TimeUnit.MILLISECONDS);
434 }, filterDelay, delayUnit);
438 sc = new ScrolledComposite(parent, SWT.V_SCROLL);
439 GridDataFactory.fillDefaults().grab(true, true).applyTo(sc);
440 sc.setAlwaysShowScrollBars(false);
441 sc.setExpandHorizontal(false);
442 sc.setExpandVertical(false);
443 sc.getVerticalBar().setIncrement(30);
444 sc.getVerticalBar().setPageIncrement(200);
445 sc.addControlListener( new ControlAdapter() {
447 public void controlResized(ControlEvent e) {
448 //System.out.println("ScrolledComposite resized: " + sc.getSize());
449 refreshScrolledComposite();
453 c = new Composite(sc, 0);
455 GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(c);
459 c.addDisposeListener(new DisposeListener() {
461 public void widgetDisposed(DisposeEvent e) {
462 // These should be in exactly this order to prevent them from
463 // screwing each other up.
464 sessionContextTracker.untrack();
465 projectTracker.untrack();
467 // Remember to shutdown the executor
468 loaderExecutor.shutdown();
474 IContextService cs = (IContextService) getSite().getService(IContextService.class);
475 cs.activateContext(SYMBOL_LIBRARY_CONTEXT);
479 public void dispose() {
480 if (sessionCtxProvider != null) {
481 sessionCtxProvider.removeContextChangedListener(sessionContextTracker);
482 sessionCtxProvider = null;
486 void refreshScrolledComposite() {
487 // Execute asynchronously to give the UI events triggering this method
488 // call time to run through before actually doing any resizing.
489 // Otherwise the result will lag behind reality when scrollbar
490 // visibility is toggled by the toolkit.
491 ThreadUtils.asyncExec(swtThread, new Runnable() {
496 syncRefreshScrolledComposite();
501 void syncRefreshScrolledComposite() {
502 // Execute asynchronously to give the UI events triggering this method
503 // call time to run through before actually doing any resizing.
504 // Otherwise the result will lag behind reality when scrollbar
505 // visibility is toggled by the toolkit.
506 Rectangle r = sc.getClientArea();
507 Point contentSize = c.computeSize(r.width, SWT.DEFAULT);
508 //System.out.println("[" + Thread.currentThread() + "] computed content size: " + contentSize + ", " + r);
509 c.setSize(contentSize);
513 * (Re-)Load symbol groups, refresh the content
515 void load(Collection<ISymbolGroup> _libraries) {
516 if (_libraries == null)
517 _libraries = Collections.emptyList();
518 final Collection<ISymbolGroup> libraries = _libraries;
519 loaderExecutor.execute(new Runnable() {
522 // Increment loadCount to signal that a new load cycle is on the way.
523 Integer loadId = loadCount.incrementAndGet();
525 loaderSemaphore.acquire();
526 beginPopulate(loadId);
527 } catch (InterruptedException e) {
528 ExceptionUtils.logError(e);
529 } catch (RuntimeException e) {
530 loaderSemaphore.release();
531 ExceptionUtils.logAndShowError(e);
533 loaderSemaphore.release();
534 ExceptionUtils.logAndShowError(e);
538 void beginPopulate(Integer loadId) {
539 synchronized (groups) {
540 // Must use toArray since groups are removed within the loop
541 for (Iterator<Map.Entry<ISymbolGroup, PGroup>> it = groups.entrySet().iterator(); it.hasNext();) {
542 Map.Entry<ISymbolGroup, PGroup> entry = it.next();
543 if (!libraries.contains(entry.getKey())) {
544 PGroup group = entry.getValue();
546 groupViewers.remove(entry.getKey());
547 if (group != null && !group.isDisposed())
548 ThreadUtils.asyncExec(swtThread, disposer(group));
551 Set<GroupDescriptor> groupDescs = new TreeSet<GroupDescriptor>(groupComparator);
552 for (ISymbolGroup lib : libraries) {
553 PGroup group = groups.get(lib);
554 //String label = group != null ? group.getText() : lib.getName();
555 String label = lib.getName();
556 groupDescs.add(new GroupDescriptor(lib, label, group));
559 // Populate all the missing groups.
560 IFilter groupFilter = currentGroupFilter;
564 groupDescs.iterator(),
570 loaderSemaphore.release();
579 final ExecutorService exec,
580 final Control lastGroup,
581 final Iterator<GroupDescriptor> iter,
582 final IFilter groupFilter,
583 final Integer loadId,
584 final Runnable loadComplete)
586 // Check whether to still continue this population or not.
587 int currentLoadId = loadCount.get();
588 if (currentLoadId != loadId) {
593 if (!iter.hasNext()) {
594 ThreadUtils.asyncExec(swtThread, new Runnable() {
597 if (filter.isDisposed() || c.isDisposed())
607 final GroupDescriptor desc = iter.next();
609 ThreadUtils.asyncExec(swtThread, new Runnable() {
612 // Must make sure that loadComplete is invoked under error
616 } catch (RuntimeException e) {
618 ExceptionUtils.logAndShowError(e);
621 ExceptionUtils.logAndShowError(e);
625 public void populateGroup() {
626 if (c.isDisposed()) {
627 loaderSemaphore.release();
631 //System.out.println("populating: " + desc.label);
632 PGroup group = desc.group;
633 if (group == null || group.isDisposed()) {
635 group = new PGroup(c, SWT.NONE);
636 // group.addListener(SWT.KeyUp, filterActivationListener);
637 // group.addListener(SWT.KeyDown, filterActivationListener);
638 // group.addListener(SWT.FocusIn, filterActivationListener);
639 // group.addListener(SWT.FocusOut, filterActivationListener);
640 // group.addListener(SWT.MouseDown, filterActivationListener);
641 // group.addListener(SWT.MouseUp, filterActivationListener);
642 // group.addListener(SWT.MouseDoubleClick, filterActivationListener);
643 // group.addListener(SWT.Arm, filterActivationListener);
644 if (lastGroup != null) {
645 group.moveBelow(lastGroup);
647 group.moveAbove(null);
650 groups.put(desc.lib, group);
652 group.setData(SymbolLibraryKeys.KEY_GROUP, desc.lib);
653 group.setData(KEY_USER_EXPANDED, defaultExpanded);
655 group.setExpanded(defaultExpanded);
656 group.setFont(resourceManager.createFont(FontDescriptor.createFrom(group.getFont()).setStyle(SWT.NORMAL).increaseHeight(-1)));
657 GridDataFactory.fillDefaults().align(SWT.FILL, SWT.BEGINNING).grab(true, false).applyTo(group);
658 GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(group);
659 group.addExpandListener(groupExpandListener);
661 // Track group content changes if possible.
662 if (desc.lib instanceof IModifiableSymbolGroup) {
663 IModifiableSymbolGroup mod = (IModifiableSymbolGroup) desc.lib;
664 mod.addListener(groupListener);
668 group.setText(desc.label);
669 group.setToolTipText(desc.label);
671 // Initialize the group: set content provider, label provider and input.
672 // NOTE: this should not yet start loading any data, just setup the viewer.
673 // [Tuukka @ 2009-10-24] changed group contents to be
674 // initialized lazily when needed. See references to initializeGroup(PGroup).
675 //initializeGroup(group);
677 // Hide the group immediately if necessary.
678 boolean groupFiltered = !groupFilter.select(desc.label);
679 group.setData(KEY_GROUP_FILTERED, Boolean.valueOf(groupFiltered));
681 setGroupVisible(group, false);
683 syncRefreshScrolledComposite();
685 final PGroup group_ = group;
686 exec.execute(new Runnable() {
689 populateGroups(exec, group_, iter, groupFilter, loadId, loadComplete);
698 * @return <code>null</code> if GalleryViewer is currently being created
700 GalleryViewer initializeGroup(final PGroup group) {
701 if (group.isDisposed())
704 //System.out.println("initializeGroup(" + group + ")");
706 synchronized (group) {
707 if (group.getData(KEY_VIEWER_INITIALIZED) != null) {
708 return (GalleryViewer) group.getData(SymbolLibraryKeys.KEY_GALLERY);
710 group.setData(KEY_VIEWER_INITIALIZED, Boolean.TRUE);
713 //System.out.println("initializing group: " + group.getText());
715 // NOTE: this will wait until the SWT/AWT UI population has completed
716 // and it will dispatch SWT events while waiting for AWT thread population
717 // to complete. This may in turn cause other parties to invoke this same
718 // initializeGroup method with the same group as parameter. This case
719 // needs to be handled appropriately by callers.
720 GalleryViewer viewer = new GalleryViewer(group);
722 ISymbolGroup input = (ISymbolGroup) group.getData(SymbolLibraryKeys.KEY_GROUP);
723 initializeViewer(group, input, viewer);
725 groupViewers.put(input, viewer);
726 group.setData(SymbolLibraryKeys.KEY_GALLERY, viewer);
728 //System.out.println("initialized group: " + group.getText());
733 void initializeViewer(final PGroup group, final ISymbolGroup input, final GalleryViewer viewer) {
734 GridDataFactory.fillDefaults().align(SWT.FILL, SWT.BEGINNING).grab(true, false).applyTo(viewer.getControl());
735 viewer.addDragSupport(new DragSourceParticipant());
736 viewer.setAlign(FlowLayout.Align.Left);
737 viewer.getDiagram().setHint(SynchronizationHints.ERROR_HANDLER, errorHandler);
739 viewer.setContentProvider(new IStructuredContentProvider() {
742 * Returns the elements in the input, which must be either an array or a
743 * <code>Collection</code>.
746 public Object[] getElements(Object inputElement) {
747 if(inputElement == null) return new Object[0];
748 return ((ISymbolGroup)inputElement).getItems();
752 * This implementation does nothing.
755 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
760 * This implementation does nothing.
763 public void dispose() {
768 viewer.setLabelProvider(new LabelProvider());
769 viewer.setInput(input);
771 // Add event handler that closes libraries on double clicks into empty
773 viewer.getCanvasContext().getEventHandlerStack().add(new IEventHandler() {
775 public int getEventMask() {
776 return EventTypes.MouseDoubleClickMask;
779 public boolean handleEvent(org.simantics.scenegraph.g2d.events.Event e) {
780 if (e instanceof MouseDoubleClickedEvent) {
781 PickRequest req = new PickRequest(((MouseDoubleClickedEvent) e).controlPosition);
782 Collection<IElement> result = new ArrayList<IElement>();
783 DiagramUtils.pick(viewer.getDiagram(), req, result);
784 if (!result.isEmpty())
787 //System.out.println("NOTHING CLICKED");
788 if (group.isDisposed())
790 group.getDisplay().asyncExec(new Runnable() {
793 if (group.isDisposed())
796 boolean exp = !group.getExpanded();
797 group.setData(KEY_USER_EXPANDED, Boolean.valueOf(exp));
798 setGroupExpandedWithoutNotification(group, exp);
799 refreshScrolledComposite();
809 static String toPatternString(String filter) {
810 if (!filter.isEmpty()) {
811 // Force searching in lowercase.
812 filter = filter.toLowerCase();
814 // Construct a regular expression from the specified text.
815 String regExFilter = filter
816 .replace("\\", "\\\\") // \ -> \\
817 .replace(".", "\\.") // . -> \.
818 .replace("*", ".*") // * -> Any 0..n characters
819 .replace("?", ".") // ? -> Any single character
820 .replace("+", "\\+") // + -> \+
821 .replace("(", "\\(") // ( -> \(
822 .replace(")", "\\)") // ) -> \)
823 .replace("[", "\\[") // [ -> \[
824 .replace("]", "\\]") // ] -> \]
825 .replace("{", "\\{") // { -> \{
826 .replace("}", "\\}") // } -> \}
827 .replace("^", "\\^") // ^ -> \^
828 .replace("$", "\\$") // $ -> \$
829 .replace("|", ".*|") // $ -> \$
830 //.replace("|", "\\|") // | -> \|
831 .replace("&&", "\\&&") // && -> \&&
834 if (!regExFilter.startsWith(".*"))
835 regExFilter = ".*" + regExFilter ;
836 if (!regExFilter.endsWith(".*"))
837 regExFilter += ".*" ;
844 static class SymbolItemFilter extends ViewerFilter {
845 private final String string;
846 private final Pattern pattern;
848 public SymbolItemFilter(String string, Pattern pattern) {
849 this.string = string;
850 this.pattern = pattern;
854 public boolean select(Viewer viewer, Object parentElement, Object element) {
855 ISymbolItem item = (ISymbolItem) element;
856 String name = item.getName();
857 Matcher m = pattern.matcher(name.toLowerCase());
858 //System.out.println(name + ": " + (m.matches() ? "PASS" : "FAIL"));
863 public int hashCode() {
864 return string == null ? 0 : string.hashCode();
868 public boolean equals(Object obj) {
873 if (getClass() != obj.getClass())
875 SymbolItemFilter other = (SymbolItemFilter) obj;
876 if (string == null) {
877 if (other.string != null)
879 } else if (!string.equals(other.string))
885 static Pattern toPattern(String filterText) {
886 String regExFilter = toPatternString(filterText);
887 Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY;
891 static IFilter composeFilter(final FilterConfiguration config) {
892 final Mode mode = config.getMode();
893 final List<Pattern> patterns = new ArrayList<Pattern>();
894 for (GroupFilter f : config.getFilters()) {
896 patterns.add(toPattern(f.getFilterText()));
898 return new IFilter() {
900 public boolean select(Object toTest) {
901 if (patterns.isEmpty())
904 String s = (String) toTest;
907 for (Pattern pat : patterns) {
908 Matcher m = pat.matcher(s.toLowerCase());
909 //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL"));
915 for (Pattern pat : patterns) {
916 Matcher m = pat.matcher(s.toLowerCase());
917 //System.out.println(s + ": " + (m.matches() ? "PASS" : "FAIL"));
923 throw new Error("Shouldn't happen");
929 void updateFilterConfiguration(FilterConfiguration config) {
930 this.config = config;
931 IFilter filter = composeFilter(config);
932 this.currentGroupFilter = filter;
935 void applyGroupFilters() {
936 IFilter groupFilter = this.currentGroupFilter;
937 final boolean[] changed = new boolean[] { false };
939 Control[] grps = c.getChildren();
940 for (Control ctrl : grps) {
941 final PGroup grp = (PGroup) ctrl;
942 boolean visible = grp.getVisible();
943 boolean shouldBeVisible = groupFilter.select(grp.getText());
944 boolean change = visible != shouldBeVisible;
945 changed[0] |= change;
947 grp.setData(KEY_GROUP_FILTERED, Boolean.valueOf(!shouldBeVisible));
949 setGroupVisible(grp, shouldBeVisible);
953 ThreadUtils.asyncExec(swtThread, new Runnable() {
960 syncRefreshScrolledComposite();
967 * Filters the symbol groups and makes them visible/invisible as necessary.
968 * Invoke only from the SWT thread.
970 * @param text the filter text given by the client
971 * @return <code>true</code> if all groups were successfully filtered
972 * without asynchronous results
974 boolean filterGroups(String text) {
975 //System.out.println("FILTERING WITH TEXT: " + text);
977 String regExFilter = toPatternString(text);
978 Pattern pattern = regExFilter != null ? Pattern.compile(regExFilter) : ANY;
980 this.currentFilterPattern = pattern;
981 final boolean[] changed = new boolean[] { false };
982 boolean filteringComplete = true;
984 ViewerFilter filter = null;
985 if (regExFilter != null)
986 filter = new SymbolItemFilter(regExFilter, pattern);
988 Control[] grps = c.getChildren();
989 for (Control ctrl : grps) {
990 final PGroup grp = (PGroup) ctrl;
991 if (grp.isDisposed())
993 Boolean contentsChanged = filterGroup(grp, filter);
994 if (contentsChanged == null)
995 filteringComplete = false;
997 changed[0] = contentsChanged;
1000 ThreadUtils.asyncExec(swtThread, new Runnable() {
1007 syncRefreshScrolledComposite();
1012 return filteringComplete;
1015 static boolean objectEquals(Object o1, Object o2) {
1016 if (o1==o2) return true;
1017 if (o1==null && o2==null) return true;
1018 if (o1==null || o2==null) return false;
1019 return o1.equals(o2);
1024 * @return <code>true</code> if the filtering caused changes in the group,
1025 * <code>false</code> if not, and <code>null</code> if filtering
1026 * could not be performed yet, meaning results need to be asked
1029 private Boolean filterGroup(PGroup grp, ViewerFilter filter) {
1030 boolean changed = false;
1031 GalleryViewer viewer = initializeGroup(grp);
1035 ViewerFilter lastFilter = viewer.getFilter();
1037 boolean groupFiltered = Boolean.TRUE.equals(grp.getData(KEY_GROUP_FILTERED));
1038 boolean userExpanded = Boolean.TRUE.equals(grp.getData(KEY_USER_EXPANDED));
1039 final boolean expanded = grp.getExpanded();
1040 final boolean visible = grp.getVisible();
1041 final boolean filterChanged = !objectEquals(filter, lastFilter);
1043 // Find out how much data would be shown with the new filter.
1044 viewer.setFilter(filter);
1045 Object[] elements = viewer.getFilteredElements();
1047 boolean shouldBeVisible = !groupFiltered && elements.length > 0;
1048 boolean shouldBeExpanded = shouldBeVisible && (filter != null || userExpanded);
1050 // System.out.format("%40s: visible/should be = %5s %5s, expanded/user expanded/should be = %5s %5s %5s\n",
1052 // String.valueOf(visible),
1053 // String.valueOf(shouldBeVisible),
1054 // String.valueOf(expanded),
1055 // String.valueOf(userExpanded),
1056 // String.valueOf(shouldBeExpanded));
1058 if (filterChanged || visible != shouldBeVisible || expanded != shouldBeExpanded) {
1061 if (shouldBeVisible == userExpanded) {
1062 if (expanded != shouldBeExpanded)
1063 setGroupExpandedWithoutNotification(grp, shouldBeExpanded);
1064 setGroupVisible(grp, shouldBeVisible);
1066 if (filter != null) {
1067 if (shouldBeVisible) {
1068 // The user has not expanded this group but the group contains
1069 // stuff that matches the non-empty filter => show the group.
1070 setGroupExpandedWithoutNotification(grp, true);
1071 setGroupVisible(grp, true);
1073 // The user has expanded this group but it does not contain items
1074 // should should be shown with the current non-empty filter => hide the group.
1075 setGroupExpandedWithoutNotification(grp, true);
1076 setGroupVisible(grp, false);
1079 // All groups should be visible. Some should be expanded and others not.
1080 if (expanded != userExpanded)
1081 setGroupExpandedWithoutNotification(grp, userExpanded);
1083 setGroupVisible(grp, true);
1087 if (shouldBeExpanded) {
1088 viewer.refreshWithContent(elements);
1092 // String label = grp.getText();
1093 // Matcher m = pattern.matcher(label.toLowerCase());
1094 // boolean visible = m.matches();
1095 // if (visible != grp.getVisible()) {
1097 // setGroupVisible(grp, visible);
1103 void setGroupExpandedWithoutNotification(PGroup grp, boolean expanded) {
1104 // Ok, don't need to remove/add expand listener, PGroup will not notify
1105 // listeners when setExpanded is invoked.
1106 //grp.removeExpandListener(groupExpandListener);
1107 grp.setExpanded(expanded);
1108 //grp.addExpandListener(groupExpandListener);
1111 void setGroupVisible(PGroup group, boolean visible) {
1112 GridData gd = (GridData) group.getLayoutData();
1113 gd.exclude = !visible;
1114 group.setVisible(visible);
1117 boolean isGroupFiltered(String label) {
1118 return !currentFilterPattern.matcher(label.toLowerCase()).matches();
1121 class DragSourceParticipant extends AbstractDiagramParticipant implements IDragSourceParticipant {
1122 @Reference Selection selection;
1123 @Dependency PointerInteractor pi;
1124 @Dependency TransformUtil util;
1125 @Dependency PickContext pickContext;
1128 public int canDrag(MouseDragBegin me) {
1129 if (me.button != MouseEvent.LEFT_BUTTON) return 0;
1130 if (getHint(Hints.KEY_TOOL) != Hints.POINTERTOOL) return 0;
1131 assertDependencies();
1133 PickRequest req = new PickRequest(me.startCanvasPos);
1134 req.pickPolicy = PickRequest.PickPolicy.PICK_INTERSECTING_OBJECTS;
1135 List<IElement> picks = new ArrayList<IElement>();
1136 pickContext.pick(diagram, req, picks);
1137 Set<IElement> sel = selection.getSelection(me.mouseId);
1139 if (Collections.disjoint(sel, picks)) return 0;
1141 return DnDConstants.ACTION_LINK;
1145 public Transferable dragStart(DragGestureEvent e) {
1146 AWTChassis chassis = (AWTChassis) e.getComponent();
1147 ICanvasContext cc = chassis.getCanvasContext();
1148 Selection sel = cc.getSingleItem(Selection.class);
1150 Set<IElement> ss = sel.getSelection(0);
1151 if (ss.isEmpty()) return null;
1152 Object[] res = new Object[ss.size()];
1154 for (IElement ee : ss)
1155 res[index++] = ee.getHint(ElementHints.KEY_OBJECT);
1157 ISelection object = new StructuredSelection(res);
1159 return new LocalObjectTransferable(object);
1163 public int getAllowedOps() {
1164 return DnDConstants.ACTION_COPY;
1167 public void dragDropEnd(DragSourceDropEvent dsde) {
1168 // System.out.println("dragDropEnd: " + dsde);
1169 LocalObjectTransfer.getTransfer().clear();
1172 public void dragEnter(DragSourceDragEvent dsde) {
1175 public void dragExit(DragSourceEvent dse) {
1178 public void dragOver(DragSourceDragEvent dsde) {
1181 public void dropActionChanged(DragSourceDragEvent dsde) {
1185 ExpandListener groupExpandListener = new ExpandListener() {
1187 public void itemCollapsed(ExpandEvent e) {
1188 final PGroup group = (PGroup) e.widget;
1189 group.setData(KEY_USER_EXPANDED, Boolean.FALSE);
1190 //System.out.println("item collapsed: " + group + ", " + sc.getClientArea());
1191 refreshScrolledComposite();
1194 public void itemExpanded(ExpandEvent e) {
1195 final PGroup group = (PGroup) e.widget;
1196 group.setData(KEY_USER_EXPANDED, Boolean.TRUE);
1197 //System.out.println("item expanded: " + group + ", " + sc.getClientArea());
1198 final GalleryViewer viewer = initializeGroup(group);
1201 ThreadUtils.asyncExec(swtThread, new Runnable() {
1205 refreshScrolledComposite();
1212 public void setFocus() {
1216 public boolean isDefaultExpanded() {
1217 return defaultExpanded;
1220 public void setDefaultExpanded(boolean defaultExpanded) {
1221 this.defaultExpanded = defaultExpanded;
1224 Runnable disposer(final Widget w) {
1225 return new Runnable() {
1235 void contributeActions() {
1236 IToolBarManager toolbar = getViewSite().getActionBars().getToolBarManager();
1237 toolbar.add(new Action("Collapse All",
1238 BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/collapseall.gif")) {
1241 setAllExpandedStates(false);
1244 toolbar.add(new Action("Expand All",
1245 BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/expandall.gif")) {
1248 setAllExpandedStates(true);
1251 toolbar.add(new Action("Configure Filters",
1252 BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/filter_ps.gif")) {
1255 FilterConfiguration clone = new FilterConfiguration(config);
1256 FilterDialog fd = new FilterDialog(getViewSite().getShell(), Activator.getDefault().getDialogSettings(), clone);
1257 int result = fd.open();
1258 if (result != Window.OK)
1261 updateFilterConfiguration(clone);
1262 applyGroupFilters();
1266 } catch (BackingStoreException e) {
1267 ExceptionUtils.logAndShowError(e);
1273 void setAllExpandedStates(boolean targetState) {
1274 Boolean targetStateObj = Boolean.valueOf(targetState);
1275 boolean changed = false;
1276 setDefaultExpanded(targetState);
1277 Control[] grps = c.getChildren();
1278 //for (final PGroup grp : groups.values().toArray(new PGroup[0])) {
1279 for (Control control : grps) {
1280 final PGroup grp = (PGroup) control;
1281 grp.setData(KEY_USER_EXPANDED, targetStateObj);
1282 if (!grp.isDisposed() && grp.getExpanded() != targetState && grp.getVisible()) {
1283 final GalleryViewer viewer = initializeGroup(grp);
1284 setGroupExpandedWithoutNotification(grp, targetState);
1285 ThreadUtils.asyncExec(swtThread, new Runnable() {
1288 if (!grp.isDisposed()) {
1291 refreshScrolledComposite();
1299 refreshScrolledComposite();
1302 class ImageLoader implements Runnable {
1304 private final ImageProxy imageProxy;
1305 private final ISymbolItem item;
1307 public ImageLoader(ImageProxy imageProxy, ISymbolItem item) {
1308 this.imageProxy = imageProxy;
1314 ThreadUtils.getBlockingWorkExecutor().execute(new Runnable() {
1322 private void runBlocking() {
1324 IHintObservable hints = null;
1325 ISymbolGroup group = item.getGroup();
1327 throw new ProvisionException("No ISymbolGroup available for ISymbolItem " + item);
1329 GalleryViewer viewer = groupViewers.get(group);
1330 if (viewer == null) {
1331 // This is normal if this composite has been disposed while these are being ran.
1332 //throw new ProvisionException("No GalleryViewer available ISymbolGroup " + group);
1333 imageProxy.setSource(DefaultImages.UNKNOWN2.get());
1337 hints = viewer.getDiagram();
1339 throw new ProvisionException("No diagram available for GalleryViewer of group " + group);
1341 ElementClass ec = item.getElementClass(hints);
1342 StaticSymbol ss = ec.getSingleItem(StaticSymbol.class);
1343 Image source = ss == null ? DefaultImages.UNKNOWN2.get() : ss.getImage();
1344 imageProxy.setSource(source);
1345 } catch (ProvisionException e) {
1346 ExceptionUtils.logWarning("Failed to provide element class for symbol item " + item, e);
1347 imageProxy.setSource(DefaultImages.ERROR_DECORATOR.get());
1348 } catch (Exception e) {
1349 ExceptionUtils.logError(e);
1350 imageProxy.setSource(DefaultImages.ERROR_DECORATOR.get());
1356 public FilterArea getFilterArea() {
1360 Runnable filterActivator = new Runnable() {
1366 Listener filterActivationListener = new Listener() {
1368 public void handleEvent(Event event) {
1369 //System.out.println("event: " + event);
1370 filterActivator.run();
1374 ISymbolGroupListener groupListener = new ISymbolGroupListener() {
1376 public void itemsChanged(ISymbolGroup group) {
1377 GalleryViewer viewer = groupViewers.get(group);
1378 if (viewer != null) {
1379 ISymbolItem[] input = group.getItems();
1380 viewer.setInput(input);
1386 @SuppressWarnings("rawtypes")
1387 public Object getAdapter(Class adapter) {
1388 // For supporting Scene Graph viewer
1389 if (adapter == INode[].class) {
1390 List<INode> result = new ArrayList<INode>(groupViewers.size());
1391 for (GalleryViewer viewer : groupViewers.values()) {
1392 result.add(viewer.getCanvasContext().getSceneGraph());
1394 return result.toArray(new INode[result.size()]);
1396 return super.getAdapter(adapter);