1 /*******************************************************************************
\r
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
\r
3 * in Industry THTH ry.
\r
4 * All rights reserved. This program and the accompanying materials
\r
5 * are made available under the terms of the Eclipse Public License v1.0
\r
6 * which accompanies this distribution, and is available at
\r
7 * http://www.eclipse.org/legal/epl-v10.html
\r
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.selectionview;
\r
14 import java.util.ArrayList;
\r
15 import java.util.Collection;
\r
16 import java.util.Collections;
\r
17 import java.util.List;
\r
18 import java.util.TreeSet;
\r
19 import java.util.concurrent.CopyOnWriteArrayList;
\r
20 import java.util.concurrent.atomic.AtomicBoolean;
\r
21 import java.util.concurrent.atomic.AtomicInteger;
\r
22 import java.util.function.Consumer;
\r
24 import org.eclipse.jface.layout.GridDataFactory;
\r
25 import org.eclipse.jface.layout.GridLayoutFactory;
\r
26 import org.eclipse.jface.resource.JFaceResources;
\r
27 import org.eclipse.jface.resource.LocalResourceManager;
\r
28 import org.eclipse.jface.viewers.ISelection;
\r
29 import org.eclipse.jface.viewers.ISelectionChangedListener;
\r
30 import org.eclipse.jface.viewers.ISelectionProvider;
\r
31 import org.eclipse.jface.viewers.SelectionChangedEvent;
\r
32 import org.eclipse.jface.viewers.StructuredSelection;
\r
33 import org.eclipse.swt.SWT;
\r
34 import org.eclipse.swt.layout.FillLayout;
\r
35 import org.eclipse.swt.widgets.Composite;
\r
36 import org.eclipse.swt.widgets.Control;
\r
37 import org.eclipse.swt.widgets.Event;
\r
38 import org.eclipse.swt.widgets.Listener;
\r
39 import org.eclipse.ui.IWorkbenchPartSite;
\r
40 import org.eclipse.ui.part.IPageSite;
\r
41 import org.simantics.browsing.ui.common.ErrorLogger;
\r
42 import org.simantics.browsing.ui.swt.TabbedPropertyPage;
\r
43 import org.simantics.db.ReadGraph;
\r
44 import org.simantics.db.common.request.ReadRequest;
\r
45 import org.simantics.db.common.request.UniqueRead;
\r
46 import org.simantics.db.exception.DatabaseException;
\r
47 import org.simantics.db.management.ISessionContext;
\r
48 import org.simantics.ui.SimanticsUI;
\r
49 import org.simantics.utils.ObjectUtils;
\r
50 import org.simantics.utils.datastructures.MapList;
\r
51 import org.simantics.utils.ui.ISelectionUtils;
\r
52 import org.simantics.utils.ui.jface.BasePostSelectionProvider;
\r
56 * Subclasses may extend or reimplement the following methods as required:
\r
58 * <li><code>createBaseComposite</code> - reimplement to customize how the
\r
59 * composite is constructed that is the basis of the whole tab container.</
\r
64 * @author Tuukka Lehtonen
\r
66 public class TabbedPropertyTable extends Composite implements IPropertyTab {
\r
68 public static final SelectionProcessor<Object, Object> DEFAULT_SELECTION_PROCESSOR = new SelectionProcessor<Object, Object>() {
\r
70 public Collection<?> process(Object selection, Object object) {
\r
71 return Collections.emptyList();
\r
75 @SuppressWarnings("rawtypes")
\r
76 private SelectionProcessor selectionProcessor = DEFAULT_SELECTION_PROCESSOR;
\r
78 private Composite baseComposite;
\r
80 private final List<IPropertyTab> tabs = new CopyOnWriteArrayList<IPropertyTab>();
\r
82 private final AtomicInteger activeTab = new AtomicInteger(-1);
\r
84 protected IWorkbenchPartSite sourceSite;
\r
86 protected IPageSite pageSite;
\r
88 protected ISelection currentSelection;
\r
91 * The selection provider set for the page site.
\r
93 protected BasePostSelectionProvider pageSelectionProvider = new BasePostSelectionProvider() {
\r
96 * For preventing infinite recursion, not that it should happen.
\r
98 private AtomicBoolean settingSelection = new AtomicBoolean();
\r
101 * Overridden like this because pageSelectionProvider is published to
\r
102 * the workbench as the page site selection provider and therefore it
\r
103 * possible that its {@link ISelectionProvider#setSelection(ISelection)}
\r
104 * method is invoked externally and we need to propagate the selection
\r
105 * to the underlying active tab and its selection provider instead of
\r
106 * setting pageSelectionProvider's selection to anything.
\r
109 public void setSelection(ISelection selection) {
\r
110 if (settingSelection.compareAndSet(false, true)) {
\r
111 IPropertyTab table = getActiveTab();
\r
112 if (table != null && table.getSelectionProvider() != null)
\r
113 table.getSelectionProvider().setSelection(selection);
\r
114 settingSelection.set(false);
\r
116 ErrorLogger.defaultLogWarning("Possible BUG: prevented recursive attempt to set selection for "
\r
117 + TabbedPropertyTable.this.toString(), new Exception("trace"));
\r
122 // protected ISelectionChangedListener debugPageSelectionListener = new ISelectionChangedListener() {
\r
124 // pageSelectionProvider.addSelectionChangedListener(this);
\r
127 // public void selectionChanged(SelectionChangedEvent event) {
\r
128 // System.out.println("page selection change: " + event);
\r
129 // System.out.println(" provider: " + event.getSelectionProvider());
\r
130 // System.out.println(" selection: " + event.getSelection());
\r
134 protected ISelectionChangedListener activeTabSelectionListener = new ISelectionChangedListener() {
\r
136 public void selectionChanged(SelectionChangedEvent event) {
\r
137 // System.out.println("active tab selection change: " + event);
\r
138 // System.out.println(" provider: " + event.getSelectionProvider());
\r
139 // System.out.println(" selection: " + event.getSelection());
\r
140 ISelection s = event.getSelection();
\r
141 // This is a workaround to avert calling pageSelectionProvider.setSelection here.
\r
142 pageSelectionProvider.setSelectionWithoutFiring(s);
\r
143 pageSelectionProvider.fireSelection(s);
\r
144 pageSelectionProvider.firePostSelection(s);
\r
148 protected LocalResourceManager resourceManager;
\r
150 public TabbedPropertyTable(IWorkbenchPartSite site, IPageSite pageSite, Composite parent, int style) {
\r
151 super(parent, style);
\r
153 throw new IllegalArgumentException("null source site");
\r
154 if (pageSite == null)
\r
155 throw new IllegalArgumentException("null page site");
\r
156 this.sourceSite = site;
\r
157 this.pageSite = pageSite;
\r
158 GridLayoutFactory.fillDefaults().applyTo(this);
\r
160 resourceManager = new LocalResourceManager(JFaceResources.getResources(parent.getDisplay()));
\r
162 addListener(SWT.Dispose, new Listener() {
\r
164 public void handleEvent(Event event) {
\r
165 //System.out.println("DISPOSING " + this + " " + System.identityHashCode(TabbedPropertyTable.this));
\r
169 currentSelection = null;
\r
170 if (currentListener != null)
\r
171 currentListener.dispose();
\r
173 TabbedPropertyTable.this.pageSite = null;
\r
174 TabbedPropertyTable.this.sourceSite = null;
\r
175 resourceManager.dispose();
\r
181 @SuppressWarnings("rawtypes")
\r
182 protected void setSelectionProcessor(SelectionProcessor selectionProcessor) {
\r
183 this.selectionProcessor = selectionProcessor;
\r
187 public void createControl(Composite parent, ISessionContext context) {
\r
188 createBaseComposite(parent, null);
\r
191 class InputListener implements org.simantics.db.procedure.Listener<Collection<?>> {
\r
193 final private Consumer<Collection<?>> inputCallback;
\r
194 private boolean disposed = false;
\r
196 public InputListener(Consumer<Collection<?>> inputCallback) {
\r
197 this.inputCallback = inputCallback;
\r
201 public void exception(Throwable t) {
\r
202 ErrorLogger.defaultLogError(t);
\r
206 public void execute(Collection<?> result) {
\r
207 inputCallback.accept(result);
\r
211 public boolean isDisposed() {
\r
212 return disposed || TabbedPropertyTable.this.isDisposed();
\r
215 public void dispose() {
\r
221 InputListener currentListener = null;
\r
224 * Must be invoked from the SWT UI thread.
\r
226 * @param selection the new selection
\r
227 * @param force <code>true</code> to force the resetting of the new input
\r
228 * even if it is the same as the previous one.
\r
231 @SuppressWarnings("unchecked")
\r
232 public void setInput(ISessionContext context, ISelection selection, boolean force) {
\r
233 //System.out.println(hashCode() + "# TabbedPropertyTable.setInput(" + selection + ", " + force + ")");
\r
236 if (context == null)
\r
239 // Check if this is a duplicate of the previous selection to reduce unnecessary flicker.
\r
240 if (!force && ObjectUtils.objectEquals(currentSelection, selection))
\r
243 // System.out.println("[3] setInput " + selection + ", force=" + force);
\r
244 currentSelection = selection;
\r
246 if (selectionProcessor != null) {
\r
248 if (currentListener != null) currentListener.dispose();
\r
250 final Collection<Object> contents = ISelectionUtils.convertSelection(selection);
\r
251 if (contents.isEmpty())
\r
254 currentListener = new InputListener(inputCallback(contents, context));
\r
256 // NOTE: must be an anonymous read to guarantee that each request
\r
257 // will always be performed and not taken from DB caches.
\r
258 context.getSession().asyncRequest(new ReadRequest() {
\r
261 public void run(ReadGraph graph) throws DatabaseException {
\r
263 graph.syncRequest(new UniqueRead<Collection<?>>() {
\r
265 public Collection<?> perform(ReadGraph graph) throws DatabaseException {
\r
266 //System.out.println("TabbedPropertyTable.setInput.perform(" + contents + ")");
\r
267 return selectionProcessor.process(contents, graph);
\r
269 }, currentListener);
\r
277 protected Consumer<Collection<?>> inputCallback(final Collection<Object> selectionContents, final ISessionContext sessionContext) {
\r
278 return new Consumer<Collection<?>>() {
\r
280 public void accept(final Collection<?> contribs) {
\r
284 // if (contribs.isEmpty())
\r
287 SimanticsUI.asyncExecSWT(TabbedPropertyTable.this, new Runnable() {
\r
289 public void run() {
\r
291 //System.out.println(Thread.currentThread() + " inputCallback: " + input);
\r
292 //System.out.println("is TabbedPropertyTable " + this + " visible: " + isVisible());
\r
293 if (!isVisible()) {
\r
294 // Set current selection to null to force update when the
\r
295 // page becomes visible
\r
296 currentSelection = null;
\r
300 createBaseComposite(TabbedPropertyTable.this, new TabbedPropertyPage(sourceSite.getPart()) {
\r
303 * The selection providers for the current set of property tabs.
\r
305 ISelectionProvider[] tabSelectionProviders = { null };
\r
308 protected void initializePageSwitching() {
\r
309 // Overridden to prevent TabbedPropertyPage
\r
310 // from initializing PageSwitcher
\r
314 protected int getContainerStyle() {
\r
315 return TabbedPropertyTable.this.getTabFolderStyle();
\r
319 protected void pageChange(int newPageIndex) {
\r
320 int oldActiveTab = activeTab.getAndSet(newPageIndex);
\r
321 //System.out.println(Thread.currentThread() + " page changed: from " + oldActiveTab + " to " + newPageIndex);
\r
322 super.pageChange(newPageIndex);
\r
324 // Remove possible old selection listeners from the hidden tab.
\r
325 ISelection oldSelection = null;
\r
326 IPropertyTab oldTab = null;
\r
327 if (oldActiveTab > -1) {
\r
328 oldTab = tabs.get(oldActiveTab);
\r
329 ISelectionProvider pv = oldTab.getSelectionProvider();
\r
331 oldSelection = pv.getSelection();
\r
332 pv.removeSelectionChangedListener(activeTabSelectionListener);
\r
336 // Attach selection listeners to the activated tab if possible.
\r
337 ISelection activeSelection = null;
\r
338 IPropertyTab activeTab = getActiveTab();
\r
339 if (activeTab != null) {
\r
340 ISelectionProvider pv = activeTab.getSelectionProvider();
\r
342 activeSelection = pv.getSelection();
\r
343 pv.addSelectionChangedListener(activeTabSelectionListener);
\r
347 String oldLabel = null;
\r
348 String newLabel = null;
\r
349 if (oldActiveTab > -1)
\r
350 oldLabel = getPageText(oldActiveTab);
\r
351 if (newPageIndex > -1)
\r
352 newLabel = getPageText(newPageIndex);
\r
353 activeTabChanged(new TabChangeEvent(oldTab, oldLabel, activeTab, newLabel));
\r
355 // This is a workaround to avert calling pageSelectionProvider.setSelection here.
\r
356 pageSelectionProvider.setSelectionWithoutFiring(activeSelection);
\r
357 if (!ObjectUtils.objectEquals(oldSelection, activeSelection)) {
\r
358 pageSelectionProvider.fireSelection(activeSelection);
\r
359 pageSelectionProvider.firePostSelection(activeSelection);
\r
364 protected void createPages() {
\r
366 // 1. loop through a list of possible contributors
\r
367 // 2. list the ones that can contribute, in priority order told by the contributions themselves
\r
368 // 3. add pages in priority order
\r
369 // 4. initialize pages
\r
371 // Categorize contributions by id
\r
372 MapList<String,ComparableTabContributor> ml = new MapList<String,ComparableTabContributor>();
\r
373 for (Object o : contribs) {
\r
374 if (o instanceof ComparableTabContributor) {
\r
375 ComparableTabContributor c = (ComparableTabContributor) o;
\r
376 ml.add(c.getId(), c);
\r
378 System.out.println("WARNING: SelectionProcessor produced an unusable contribution to TabbedPropertyTable: " + o);
\r
382 // For each id take the greatest (id) contribution
\r
383 Collection<ComparableTabContributor> contributions = new TreeSet<ComparableTabContributor>();
\r
384 for(String key : ml.getKeys()) {
\r
385 TreeSet<ComparableTabContributor> ts = new TreeSet<ComparableTabContributor>(ml.getValuesUnsafe(key));
\r
386 ComparableTabContributor contrib = ts.first();
\r
387 contributions.add(contrib);
\r
390 // Sort contributions by id
\r
391 List<ComparableTabContributor> contributionList = new ArrayList<ComparableTabContributor>(contributions);
\r
393 if (contributions.isEmpty()) {
\r
394 Composite empty = createEmptyControl(getContainer(), SWT.NONE);
\r
395 addPage(empty, "No properties to show", null);
\r
397 // Set single selection provider into pageSite
\r
398 // that is dispatched every time the selection
\r
399 // in the property view changes, be it because
\r
400 // the selection changed in the current tab or
\r
401 // because the active tab, i.e. the selection
\r
402 // provider is changed.
\r
403 pageSite.setSelectionProvider(pageSelectionProvider);
\r
405 // Allocate space for each tab to specify
\r
406 // its own selection provider.
\r
407 tabSelectionProviders = new ISelectionProvider[contributions.size()];
\r
410 for (ComparableTabContributor cc : contributionList) {
\r
411 Composite middleware = new Composite(getContainer(), SWT.NONE);
\r
412 GridLayoutFactory.fillDefaults().applyTo(middleware);
\r
414 Control control = null;
\r
416 // Create a wrapper for pageSite to make it possible
\r
417 // for each tab contribution to specify its own
\r
418 // selection provider.
\r
419 final int tabIndex = index++;
\r
420 IPageSite siteWrapper = new PageSiteProxy(pageSite) {
\r
422 public void setSelectionProvider(ISelectionProvider provider) {
\r
423 tabSelectionProviders[tabIndex] = provider;
\r
424 if(pageSelectionProvider != null && provider != null)
\r
425 pageSelectionProvider.setAndFireNonEqualSelection(provider.getSelection());
\r
428 public ISelectionProvider getSelectionProvider() {
\r
429 return tabSelectionProviders[tabIndex];
\r
433 IPropertyTab tab = cc.create(middleware, siteWrapper, sessionContext, cc.getInput());
\r
436 control = tab.getControl();
\r
437 if(control != null && !control.isDisposed()) GridDataFactory.fillDefaults().grab(true, true).applyTo(control);
\r
439 control = createEmptyControl(middleware, SWT.NONE);
\r
440 // Button b = new Button(middleware, SWT.BORDER);
\r
441 // GridDataFactory.fillDefaults().grab(true, true).applyTo(b);
\r
442 // b.setText("FOO");
\r
445 addPage(middleware, cc.getLabel(), cc.getImage());
\r
448 Object input = cc.getInput();
\r
449 tab.setInput(sessionContext, new StructuredSelection(input), false);
\r
453 ComparableTabContributor cc = TabbedPropertyTable.this.selectInitialTab(selectionContents, contributionList);
\r
455 int activePage = contributionList.indexOf(cc);
\r
456 if (activePage != -1)
\r
457 setActivePage(activePage);
\r
462 Composite createEmptyControl(Composite parent, int flags) {
\r
463 Composite empty = new Composite(parent, flags);
\r
464 GridLayoutFactory.fillDefaults().applyTo(empty);
\r
476 public Control getControl() {
\r
481 public void requestFocus() {
\r
482 IPropertyTab table = getActiveTab();
\r
484 table.requestFocus();
\r
487 public IPropertyTab getActiveTab() {
\r
488 int index = activeTab.get();
\r
489 if (index < 0 || index >= tabs.size())
\r
491 IPropertyTab tab = tabs.get(index);
\r
495 protected Composite createBaseComposite(final Composite parent, TabbedPropertyPage page) {
\r
496 // 1. dispose the previous UI container
\r
499 // 2. construct new base for the tabbed property UI
\r
500 baseComposite = new Composite(parent, SWT.NONE);
\r
503 // parent.setBackground(getDisplay().getSystemColor(SWT.COLOR_RED));
\r
504 // baseComposite.setBackground(getDisplay().getSystemColor(SWT.COLOR_TITLE_INACTIVE_BACKGROUND));
\r
508 page.createPartControl(baseComposite);
\r
510 GridLayoutFactory.fillDefaults().applyTo(parent);
\r
511 GridDataFactory.fillDefaults().grab(true, true).applyTo(baseComposite);
\r
512 baseComposite.setLayout(new FillLayout());
\r
514 return baseComposite;
\r
517 private void disposeAll() {
\r
518 if (baseComposite != null && !baseComposite.isDisposed()) {
\r
519 baseComposite.dispose();
\r
520 baseComposite = null;
\r
523 for (IPropertyTab tab : tabs)
\r
529 public ISelectionProvider getSelectionProvider() {
\r
530 return pageSelectionProvider;
\r
534 * Override to perform actions when the active tab changes.
\r
536 * @param previousTab
\r
539 protected void activeTabChanged(TabChangeEvent event) {
\r
543 * @param selectionContents
\r
544 * @param contributions
\r
547 protected ComparableTabContributor selectInitialTab(Collection<Object> selectionContents, Collection<ComparableTabContributor> contributions) {
\r
554 protected int getTabFolderStyle() {
\r