/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.selectionview; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.ui.IWorkbenchPartSite; import org.eclipse.ui.part.IPageSite; import org.simantics.browsing.ui.common.ErrorLogger; import org.simantics.browsing.ui.swt.TabbedPropertyPage; import org.simantics.db.ReadGraph; import org.simantics.db.common.request.ReadRequest; import org.simantics.db.common.request.UniqueRead; import org.simantics.db.exception.DatabaseException; import org.simantics.db.management.ISessionContext; import org.simantics.ui.SimanticsUI; import org.simantics.utils.ObjectUtils; import org.simantics.utils.datastructures.MapList; import org.simantics.utils.ui.ISelectionUtils; import org.simantics.utils.ui.jface.BasePostSelectionProvider; /** *

* Subclasses may extend or reimplement the following methods as required: *

*

* * @author Tuukka Lehtonen */ public class TabbedPropertyTable extends Composite implements IPropertyTab { public static final SelectionProcessor DEFAULT_SELECTION_PROCESSOR = new SelectionProcessor() { @Override public Collection process(Object selection, Object object) { return Collections.emptyList(); } }; @SuppressWarnings("rawtypes") private SelectionProcessor selectionProcessor = DEFAULT_SELECTION_PROCESSOR; private Composite baseComposite; private final List tabs = new CopyOnWriteArrayList(); private final AtomicInteger activeTab = new AtomicInteger(-1); protected IWorkbenchPartSite sourceSite; protected IPageSite pageSite; protected ISelection currentSelection; /** * The selection provider set for the page site. */ protected BasePostSelectionProvider pageSelectionProvider = new BasePostSelectionProvider() { /** * For preventing infinite recursion, not that it should happen. */ private AtomicBoolean settingSelection = new AtomicBoolean(); /** * Overridden like this because pageSelectionProvider is published to * the workbench as the page site selection provider and therefore it * possible that its {@link ISelectionProvider#setSelection(ISelection)} * method is invoked externally and we need to propagate the selection * to the underlying active tab and its selection provider instead of * setting pageSelectionProvider's selection to anything. */ @Override public void setSelection(ISelection selection) { if (settingSelection.compareAndSet(false, true)) { IPropertyTab table = getActiveTab(); if (table != null && table.getSelectionProvider() != null) table.getSelectionProvider().setSelection(selection); settingSelection.set(false); } else { ErrorLogger.defaultLogWarning("Possible BUG: prevented recursive attempt to set selection for " + TabbedPropertyTable.this.toString(), new Exception("trace")); } } }; // protected ISelectionChangedListener debugPageSelectionListener = new ISelectionChangedListener() { // { // pageSelectionProvider.addSelectionChangedListener(this); // } // @Override // public void selectionChanged(SelectionChangedEvent event) { // System.out.println("page selection change: " + event); // System.out.println(" provider: " + event.getSelectionProvider()); // System.out.println(" selection: " + event.getSelection()); // } // }; protected ISelectionChangedListener activeTabSelectionListener = new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { // System.out.println("active tab selection change: " + event); // System.out.println(" provider: " + event.getSelectionProvider()); // System.out.println(" selection: " + event.getSelection()); ISelection s = event.getSelection(); // This is a workaround to avert calling pageSelectionProvider.setSelection here. pageSelectionProvider.setSelectionWithoutFiring(s); pageSelectionProvider.fireSelection(s); pageSelectionProvider.firePostSelection(s); } }; protected LocalResourceManager resourceManager; public TabbedPropertyTable(IWorkbenchPartSite site, IPageSite pageSite, Composite parent, int style) { super(parent, style); if (site == null) throw new IllegalArgumentException("null source site"); if (pageSite == null) throw new IllegalArgumentException("null page site"); this.sourceSite = site; this.pageSite = pageSite; GridLayoutFactory.fillDefaults().applyTo(this); resourceManager = new LocalResourceManager(JFaceResources.getResources(parent.getDisplay())); addListener(SWT.Dispose, new Listener() { @Override public void handleEvent(Event event) { //System.out.println("DISPOSING " + this + " " + System.identityHashCode(TabbedPropertyTable.this)); activeTab.set(-1); tabs.clear(); currentSelection = null; if (currentListener != null) currentListener.dispose(); TabbedPropertyTable.this.pageSite = null; TabbedPropertyTable.this.sourceSite = null; resourceManager.dispose(); } }); } @SuppressWarnings("rawtypes") protected void setSelectionProcessor(SelectionProcessor selectionProcessor) { this.selectionProcessor = selectionProcessor; } @Override public void createControl(Composite parent, ISessionContext context) { createBaseComposite(parent, null); } class InputListener implements org.simantics.db.procedure.Listener> { final private Consumer> inputCallback; private boolean disposed = false; public InputListener(Consumer> inputCallback) { this.inputCallback = inputCallback; } @Override public void exception(Throwable t) { ErrorLogger.defaultLogError(t); } @Override public void execute(Collection result) { inputCallback.accept(result); } @Override public boolean isDisposed() { return disposed || TabbedPropertyTable.this.isDisposed(); } public void dispose() { disposed = true; } } InputListener currentListener = null; /** * Must be invoked from the SWT UI thread. * * @param selection the new selection * @param force true to force the resetting of the new input * even if it is the same as the previous one. */ @Override @SuppressWarnings("unchecked") public void setInput(ISessionContext context, ISelection selection, boolean force) { //System.out.println(hashCode() + "# TabbedPropertyTable.setInput(" + selection + ", " + force + ")"); if (isDisposed()) return; if (context == null) return; // Check if this is a duplicate of the previous selection to reduce unnecessary flicker. if (!force && ObjectUtils.objectEquals(currentSelection, selection)) return; // System.out.println("[3] setInput " + selection + ", force=" + force); currentSelection = selection; if (selectionProcessor != null) { if (currentListener != null) currentListener.dispose(); final Collection contents = ISelectionUtils.convertSelection(selection); if (contents.isEmpty()) return; currentListener = new InputListener(inputCallback(contents, context)); // NOTE: must be an anonymous read to guarantee that each request // will always be performed and not taken from DB caches. context.getSession().asyncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { graph.syncRequest(new UniqueRead>() { @Override public Collection perform(ReadGraph graph) throws DatabaseException { //System.out.println("TabbedPropertyTable.setInput.perform(" + contents + ")"); return selectionProcessor.process(contents, graph); } }, currentListener); } }); } } protected Consumer> inputCallback(final Collection selectionContents, final ISessionContext sessionContext) { return new Consumer>() { @Override public void accept(final Collection contribs) { if (isDisposed()) return; // if (contribs.isEmpty()) // return; SimanticsUI.asyncExecSWT(TabbedPropertyTable.this, new Runnable() { public void run() { //System.out.println(Thread.currentThread() + " inputCallback: " + input); //System.out.println("is TabbedPropertyTable " + this + " visible: " + isVisible()); if (!isVisible()) { // Set current selection to null to force update when the // page becomes visible currentSelection = null; return; } createBaseComposite(TabbedPropertyTable.this, new TabbedPropertyPage(sourceSite.getPart()) { /** * The selection providers for the current set of property tabs. */ ISelectionProvider[] tabSelectionProviders = { null }; @Override protected void initializePageSwitching() { // Overridden to prevent TabbedPropertyPage // from initializing PageSwitcher } @Override protected int getContainerStyle() { return TabbedPropertyTable.this.getTabFolderStyle(); } @Override protected void pageChange(int newPageIndex) { int oldActiveTab = activeTab.getAndSet(newPageIndex); //System.out.println(Thread.currentThread() + " page changed: from " + oldActiveTab + " to " + newPageIndex); super.pageChange(newPageIndex); // Remove possible old selection listeners from the hidden tab. ISelection oldSelection = null; IPropertyTab oldTab = null; if (oldActiveTab > -1) { oldTab = tabs.get(oldActiveTab); ISelectionProvider pv = oldTab.getSelectionProvider(); if (pv != null) { oldSelection = pv.getSelection(); pv.removeSelectionChangedListener(activeTabSelectionListener); } } // Attach selection listeners to the activated tab if possible. ISelection activeSelection = null; IPropertyTab activeTab = getActiveTab(); if (activeTab != null) { ISelectionProvider pv = activeTab.getSelectionProvider(); if (pv != null) { activeSelection = pv.getSelection(); pv.addSelectionChangedListener(activeTabSelectionListener); } } String oldLabel = null; String newLabel = null; if (oldActiveTab > -1) oldLabel = getPageText(oldActiveTab); if (newPageIndex > -1) newLabel = getPageText(newPageIndex); activeTabChanged(new TabChangeEvent(oldTab, oldLabel, activeTab, newLabel)); // This is a workaround to avert calling pageSelectionProvider.setSelection here. pageSelectionProvider.setSelectionWithoutFiring(activeSelection); if (!ObjectUtils.objectEquals(oldSelection, activeSelection)) { pageSelectionProvider.fireSelection(activeSelection); pageSelectionProvider.firePostSelection(activeSelection); } } @Override protected void createPages() { // 1. loop through a list of possible contributors // 2. list the ones that can contribute, in priority order told by the contributions themselves // 3. add pages in priority order // 4. initialize pages // Categorize contributions by id MapList ml = new MapList(); for (Object o : contribs) { if (o instanceof ComparableTabContributor) { ComparableTabContributor c = (ComparableTabContributor) o; ml.add(c.getId(), c); } else { System.out.println("WARNING: SelectionProcessor produced an unusable contribution to TabbedPropertyTable: " + o); } } // For each id take the greatest (id) contribution Collection contributions = new TreeSet(); for(String key : ml.getKeys()) { TreeSet ts = new TreeSet(ml.getValuesUnsafe(key)); ComparableTabContributor contrib = ts.first(); contributions.add(contrib); } // Sort contributions by id List contributionList = new ArrayList(contributions); if (contributions.isEmpty()) { Composite empty = createEmptyControl(getContainer(), SWT.NONE); addPage(empty, "No properties to show", null); } else { // Set single selection provider into pageSite // that is dispatched every time the selection // in the property view changes, be it because // the selection changed in the current tab or // because the active tab, i.e. the selection // provider is changed. pageSite.setSelectionProvider(pageSelectionProvider); // Allocate space for each tab to specify // its own selection provider. tabSelectionProviders = new ISelectionProvider[contributions.size()]; int index = 0; for (ComparableTabContributor cc : contributionList) { Composite middleware = new Composite(getContainer(), SWT.NONE); GridLayoutFactory.fillDefaults().applyTo(middleware); Control control = null; // Create a wrapper for pageSite to make it possible // for each tab contribution to specify its own // selection provider. final int tabIndex = index++; IPageSite siteWrapper = new PageSiteProxy(pageSite) { @Override public void setSelectionProvider(ISelectionProvider provider) { tabSelectionProviders[tabIndex] = provider; if(pageSelectionProvider != null && provider != null) pageSelectionProvider.setAndFireNonEqualSelection(provider.getSelection()); } @Override public ISelectionProvider getSelectionProvider() { return tabSelectionProviders[tabIndex]; } }; IPropertyTab tab = cc.create(middleware, siteWrapper, sessionContext, cc.getInput()); if (tab != null) { tabs.add(tab); control = tab.getControl(); if(control != null && !control.isDisposed()) GridDataFactory.fillDefaults().grab(true, true).applyTo(control); } else { control = createEmptyControl(middleware, SWT.NONE); // Button b = new Button(middleware, SWT.BORDER); // GridDataFactory.fillDefaults().grab(true, true).applyTo(b); // b.setText("FOO"); } addPage(middleware, cc.getLabel(), cc.getImage()); if (tab != null) { Object input = cc.getInput(); tab.setInput(sessionContext, new StructuredSelection(input), false); } } ComparableTabContributor cc = TabbedPropertyTable.this.selectInitialTab(selectionContents, contributionList); if (cc != null) { int activePage = contributionList.indexOf(cc); if (activePage != -1) setActivePage(activePage); } } } Composite createEmptyControl(Composite parent, int flags) { Composite empty = new Composite(parent, flags); GridLayoutFactory.fillDefaults().applyTo(empty); return empty; } }); layout(); } }); } }; } @Override public Control getControl() { return this; } @Override public void requestFocus() { IPropertyTab table = getActiveTab(); if (table != null) table.requestFocus(); } public IPropertyTab getActiveTab() { int index = activeTab.get(); if (index < 0 || index >= tabs.size()) return null; IPropertyTab tab = tabs.get(index); return tab; } protected Composite createBaseComposite(final Composite parent, TabbedPropertyPage page) { // 1. dispose the previous UI container disposeAll(); // 2. construct new base for the tabbed property UI baseComposite = new Composite(parent, SWT.NONE); // // parent.setBackground(getDisplay().getSystemColor(SWT.COLOR_RED)); // baseComposite.setBackground(getDisplay().getSystemColor(SWT.COLOR_TITLE_INACTIVE_BACKGROUND)); // if (page != null) page.createPartControl(baseComposite); GridLayoutFactory.fillDefaults().applyTo(parent); GridDataFactory.fillDefaults().grab(true, true).applyTo(baseComposite); baseComposite.setLayout(new FillLayout()); return baseComposite; } private void disposeAll() { if (baseComposite != null && !baseComposite.isDisposed()) { baseComposite.dispose(); baseComposite = null; } activeTab.set(-1); for (IPropertyTab tab : tabs) tab.dispose(); tabs.clear(); } @Override public ISelectionProvider getSelectionProvider() { return pageSelectionProvider; } /** * Override to perform actions when the active tab changes. * * @param previousTab * @param activeTab */ protected void activeTabChanged(TabChangeEvent event) { } /** * @param selectionContents * @param contributions * @return */ protected ComparableTabContributor selectInitialTab(Collection selectionContents, Collection contributions) { return null; } /** * @return */ protected int getTabFolderStyle() { return SWT.NONE; } }