]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.selectionview/src/org/simantics/selectionview/TabbedPropertyTable.java
Fixed multiple issues causing dangling references to discarded queries
[simantics/platform.git] / bundles / org.simantics.selectionview / src / org / simantics / selectionview / TabbedPropertyTable.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
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
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.selectionview;
13
14 import java.util.ArrayList;
15 import java.util.Collection;
16 import java.util.Collections;
17 import java.util.List;
18 import java.util.TreeSet;
19 import java.util.concurrent.CopyOnWriteArrayList;
20 import java.util.concurrent.atomic.AtomicBoolean;
21 import java.util.concurrent.atomic.AtomicInteger;
22 import java.util.function.Consumer;
23
24 import org.eclipse.jface.layout.GridDataFactory;
25 import org.eclipse.jface.layout.GridLayoutFactory;
26 import org.eclipse.jface.resource.JFaceResources;
27 import org.eclipse.jface.resource.LocalResourceManager;
28 import org.eclipse.jface.viewers.ISelection;
29 import org.eclipse.jface.viewers.ISelectionChangedListener;
30 import org.eclipse.jface.viewers.ISelectionProvider;
31 import org.eclipse.jface.viewers.SelectionChangedEvent;
32 import org.eclipse.jface.viewers.StructuredSelection;
33 import org.eclipse.swt.SWT;
34 import org.eclipse.swt.layout.FillLayout;
35 import org.eclipse.swt.widgets.Composite;
36 import org.eclipse.swt.widgets.Control;
37 import org.eclipse.swt.widgets.Event;
38 import org.eclipse.swt.widgets.Listener;
39 import org.eclipse.ui.IWorkbenchPartSite;
40 import org.eclipse.ui.part.IPageSite;
41 import org.simantics.browsing.ui.common.ErrorLogger;
42 import org.simantics.browsing.ui.swt.TabbedPropertyPage;
43 import org.simantics.db.ReadGraph;
44 import org.simantics.db.common.request.ReadRequest;
45 import org.simantics.db.common.request.UniqueRead;
46 import org.simantics.db.exception.DatabaseException;
47 import org.simantics.db.management.ISessionContext;
48 import org.simantics.ui.SimanticsUI;
49 import org.simantics.utils.ObjectUtils;
50 import org.simantics.utils.datastructures.MapList;
51 import org.simantics.utils.ui.ISelectionUtils;
52 import org.simantics.utils.ui.jface.BasePostSelectionProvider;
53
54 /**
55  * <p>
56  * Subclasses may extend or reimplement the following methods as required:
57  * <ul>
58  * <li><code>createBaseComposite</code> - reimplement to customize how the
59  * composite is constructed that is the basis of the whole tab container.</
60  * <li>
61  * </ul>
62  * </p>
63  * 
64  * @author Tuukka Lehtonen
65  */
66 public class TabbedPropertyTable extends Composite implements IPropertyTab {
67
68     public static final SelectionProcessor<Object, Object> DEFAULT_SELECTION_PROCESSOR = new SelectionProcessor<Object, Object>() {
69         @Override
70         public Collection<?> process(Object selection, Object object) {
71             return Collections.emptyList();
72         }
73     };
74
75     @SuppressWarnings("rawtypes")
76     private SelectionProcessor                 selectionProcessor          = DEFAULT_SELECTION_PROCESSOR;
77
78     private Composite                          baseComposite;
79
80     private final List<IPropertyTab>           tabs                        = new CopyOnWriteArrayList<IPropertyTab>();
81
82     private final AtomicInteger                activeTab                   = new AtomicInteger(-1);
83
84     protected IWorkbenchPartSite               sourceSite;
85
86     protected IPageSite                        pageSite;
87
88     protected ISelection                       currentSelection;
89
90     /**
91      * The selection provider set for the page site.
92      */
93     protected BasePostSelectionProvider        pageSelectionProvider = new BasePostSelectionProvider() {
94
95         /**
96          * For preventing infinite recursion, not that it should happen. 
97          */
98         private AtomicBoolean settingSelection = new AtomicBoolean();
99
100         /**
101          * Overridden like this because pageSelectionProvider is published to
102          * the workbench as the page site selection provider and therefore it
103          * possible that its {@link ISelectionProvider#setSelection(ISelection)}
104          * method is invoked externally and we need to propagate the selection
105          * to the underlying active tab and its selection provider instead of
106          * setting pageSelectionProvider's selection to anything.
107          */
108         @Override
109         public void setSelection(ISelection selection) {
110             if (settingSelection.compareAndSet(false, true)) {
111                 IPropertyTab table = getActiveTab();
112                 if (table != null && table.getSelectionProvider() != null)
113                     table.getSelectionProvider().setSelection(selection);
114                 settingSelection.set(false);
115             } else {
116                 ErrorLogger.defaultLogWarning("Possible BUG: prevented recursive attempt to set selection for "
117                         + TabbedPropertyTable.this.toString(), new Exception("trace"));
118             }
119         }
120     };
121
122 //    protected ISelectionChangedListener        debugPageSelectionListener = new ISelectionChangedListener() {
123 //        {
124 //            pageSelectionProvider.addSelectionChangedListener(this);
125 //        }
126 //        @Override
127 //        public void selectionChanged(SelectionChangedEvent event) {
128 //            System.out.println("page selection change: " + event);
129 //            System.out.println("    provider: " + event.getSelectionProvider());
130 //            System.out.println("    selection: " + event.getSelection());
131 //        }
132 //    };
133
134     protected ISelectionChangedListener        activeTabSelectionListener = new ISelectionChangedListener() {
135         @Override
136         public void selectionChanged(SelectionChangedEvent event) {
137 //            System.out.println("active tab selection change: " + event);
138 //            System.out.println("    provider: " + event.getSelectionProvider());
139 //            System.out.println("    selection: " + event.getSelection());
140             ISelection s = event.getSelection();
141             // This is a workaround to avert calling pageSelectionProvider.setSelection here.
142             pageSelectionProvider.setSelectionWithoutFiring(s);
143             pageSelectionProvider.fireSelection(s);
144             pageSelectionProvider.firePostSelection(s);
145         }
146     };
147
148     protected LocalResourceManager             resourceManager;
149
150     public TabbedPropertyTable(IWorkbenchPartSite site, IPageSite pageSite, Composite parent, int style) {
151         super(parent, style);
152         if (site == null)
153             throw new IllegalArgumentException("null source site");
154         if (pageSite == null)
155             throw new IllegalArgumentException("null page site");
156         this.sourceSite = site;
157         this.pageSite = pageSite;
158         GridLayoutFactory.fillDefaults().applyTo(this);
159
160         resourceManager = new LocalResourceManager(JFaceResources.getResources(parent.getDisplay()));
161
162         addListener(SWT.Dispose, new Listener() {
163             @Override
164             public void handleEvent(Event event) {
165                 //System.out.println("DISPOSING " + this + " " + System.identityHashCode(TabbedPropertyTable.this));
166                 activeTab.set(-1);
167                 tabs.clear();
168
169                 currentSelection = null;
170                 if (currentListener != null)
171                     currentListener.dispose();
172
173                 TabbedPropertyTable.this.pageSite = null;
174                 TabbedPropertyTable.this.sourceSite = null;
175                 resourceManager.dispose();
176                 
177             }
178         });
179     }
180
181     @SuppressWarnings("rawtypes")
182     protected void setSelectionProcessor(SelectionProcessor selectionProcessor) {
183         this.selectionProcessor = selectionProcessor;
184     }
185
186     @Override
187     public void createControl(Composite parent, ISessionContext context) {
188         createBaseComposite(parent, null);
189     }
190
191     class InputListener implements org.simantics.db.procedure.Listener<Collection<?>> {
192
193         final private Consumer<Collection<?>> inputCallback;
194         private boolean disposed = false;
195
196         public InputListener(Consumer<Collection<?>> inputCallback) {
197             this.inputCallback = inputCallback;
198         }
199
200         @Override
201         public void exception(Throwable t) {
202             ErrorLogger.defaultLogError(t);
203         }
204
205         @Override
206         public void execute(Collection<?> result) {
207             inputCallback.accept(result);
208         }
209
210         @Override
211         public boolean isDisposed() {
212             return disposed || TabbedPropertyTable.this.isDisposed();
213         }
214
215         public void dispose() {
216             disposed = true;
217         }
218
219     }
220
221     InputListener currentListener = null;
222
223     /**
224      * Must be invoked from the SWT UI thread.
225      * 
226      * @param selection the new selection
227      * @param force <code>true</code> to force the resetting of the new input
228      *        even if it is the same as the previous one.
229      */
230     @Override
231     @SuppressWarnings("unchecked")
232     public void setInput(ISessionContext context, ISelection selection, boolean force) {
233         //System.out.println(hashCode() + "# TabbedPropertyTable.setInput(" + selection + ", " + force + ")");
234         if (isDisposed())
235             return;
236         if (context == null)
237             return;
238
239         // Check if this is a duplicate of the previous selection to reduce unnecessary flicker.
240         if (!force && ObjectUtils.objectEquals(currentSelection, selection))
241             return;
242
243 //        System.out.println("[3] setInput " + selection + ", force=" + force);
244         currentSelection = selection;
245
246         if (selectionProcessor != null) {
247                 
248             if (currentListener != null) currentListener.dispose();
249                 
250             final Collection<Object> contents = ISelectionUtils.convertSelection(selection);
251             if (contents.isEmpty())
252                 return;
253
254             currentListener = new InputListener(inputCallback(contents, context));
255
256             // NOTE: must be an anonymous read to guarantee that each request
257             // will always be performed and not taken from DB caches.
258             context.getSession().asyncRequest(new ReadRequest() {
259                 
260                 @Override
261                 public void run(ReadGraph graph) throws DatabaseException {
262                         
263                         graph.syncRequest(new UniqueRead<Collection<?>>() {
264                         @Override
265                         public Collection<?> perform(ReadGraph graph) throws DatabaseException {
266                                 //System.out.println("TabbedPropertyTable.setInput.perform(" + contents + ")");
267                                 return selectionProcessor.process(contents, graph);
268                         }
269                         }, currentListener);
270                         
271                 }
272             });
273             
274         }
275     }
276
277     protected Consumer<Collection<?>> inputCallback(final Collection<Object> selectionContents, final ISessionContext sessionContext) {
278         return new Consumer<Collection<?>>() {
279             @Override
280             public void accept(final Collection<?> contribs) {
281                 
282                 if (isDisposed())
283                     return;
284 //                if (contribs.isEmpty())
285 //                    return;
286
287                 SimanticsUI.asyncExecSWT(TabbedPropertyTable.this, new Runnable() {
288                     
289                     public void run() {
290                         
291                         //System.out.println(Thread.currentThread() + " inputCallback: " + input);
292                         //System.out.println("is TabbedPropertyTable " + this + " visible: " + isVisible());
293                         if (!isVisible()) {
294                             // Set current selection to null to force update when the
295                             // page becomes visible
296                             currentSelection = null;
297                             return;
298                         }
299
300                         createBaseComposite(TabbedPropertyTable.this, new TabbedPropertyPage(sourceSite.getPart()) {
301
302                             /**
303                              * The selection providers for the current set of property tabs.
304                              */
305                             ISelectionProvider[] tabSelectionProviders = { null };
306
307                             @Override
308                             protected void initializePageSwitching() {
309                                 // Overridden to prevent TabbedPropertyPage
310                                 // from initializing PageSwitcher 
311                             }
312
313                             @Override
314                             protected int getContainerStyle() {
315                                 return TabbedPropertyTable.this.getTabFolderStyle();
316                             }
317
318                             @Override
319                             protected void pageChange(int newPageIndex) {
320                                 int oldActiveTab = activeTab.getAndSet(newPageIndex);
321                                 //System.out.println(Thread.currentThread() + " page changed: from " + oldActiveTab + " to " + newPageIndex);
322                                 super.pageChange(newPageIndex);
323
324                                 // Remove possible old selection listeners from the hidden tab.
325                                 ISelection oldSelection = null;
326                                 IPropertyTab oldTab = null;
327                                 if (oldActiveTab > -1) {
328                                     oldTab = tabs.get(oldActiveTab);
329                                     ISelectionProvider pv = oldTab.getSelectionProvider();
330                                     if (pv != null) {
331                                         oldSelection = pv.getSelection();
332                                         pv.removeSelectionChangedListener(activeTabSelectionListener);
333                                     }
334                                 }
335
336                                 // Attach selection listeners to the activated tab if possible.
337                                 ISelection activeSelection = null;
338                                 IPropertyTab activeTab = getActiveTab();
339                                 if (activeTab != null) {
340                                     ISelectionProvider pv = activeTab.getSelectionProvider();
341                                     if (pv != null) {
342                                         activeSelection = pv.getSelection();
343                                         pv.addSelectionChangedListener(activeTabSelectionListener);
344                                     }
345                                 }
346
347                                 String oldLabel = null;
348                                 String newLabel = null;
349                                 if (oldActiveTab > -1)
350                                     oldLabel = getPageText(oldActiveTab);
351                                 if (newPageIndex > -1)
352                                     newLabel = getPageText(newPageIndex);
353                                 activeTabChanged(new TabChangeEvent(oldTab, oldLabel, activeTab, newLabel));
354
355                                 // This is a workaround to avert calling pageSelectionProvider.setSelection here.
356                                 pageSelectionProvider.setSelectionWithoutFiring(activeSelection);
357                                 if (!ObjectUtils.objectEquals(oldSelection, activeSelection)) {
358                                     pageSelectionProvider.fireSelection(activeSelection);
359                                     pageSelectionProvider.firePostSelection(activeSelection);
360                                 }
361                             }
362
363                             @Override
364                             protected void createPages() {
365                                 
366                                 // 1. loop through a list of possible contributors
367                                 // 2. list the ones that can contribute, in priority order told by the contributions themselves
368                                 // 3. add pages in priority order
369                                 // 4. initialize pages
370                                 
371                                 // Categorize contributions by id
372                                 MapList<String,ComparableTabContributor> ml = new MapList<String,ComparableTabContributor>();
373                                 for (Object o : contribs) {
374                                     if (o instanceof ComparableTabContributor) {
375                                         ComparableTabContributor c = (ComparableTabContributor) o; 
376                                         ml.add(c.getId(), c);
377                                     } else {
378                                         System.out.println("WARNING: SelectionProcessor produced an unusable contribution to TabbedPropertyTable: " + o);
379                                     }
380                                 }
381
382                                 // For each id take the greatest (id) contribution
383                                 Collection<ComparableTabContributor> contributions = new TreeSet<ComparableTabContributor>();
384                                 for(String key : ml.getKeys()) {
385                                         TreeSet<ComparableTabContributor> ts = new TreeSet<ComparableTabContributor>(ml.getValuesUnsafe(key));
386                                         ComparableTabContributor contrib = ts.first();
387                                         contributions.add(contrib);
388                                 }
389
390                                 // Sort contributions by id
391                                 List<ComparableTabContributor> contributionList = new ArrayList<ComparableTabContributor>(contributions);
392
393                                 if (contributions.isEmpty()) {
394                                     Composite empty = createEmptyControl(getContainer(), SWT.NONE);
395                                     addPage(empty, "No properties to show", null);
396                                 } else {
397                                     // Set single selection provider into pageSite
398                                     // that is dispatched every time the selection
399                                     // in the property view changes, be it because
400                                     // the selection changed in the current tab or
401                                     // because the active tab, i.e. the selection
402                                     // provider is changed.
403                                     pageSite.setSelectionProvider(pageSelectionProvider);
404
405                                     // Allocate space for each tab to specify
406                                     // its own selection provider.
407                                     tabSelectionProviders = new ISelectionProvider[contributions.size()];
408                                     int index = 0;
409
410                                     for (ComparableTabContributor cc : contributionList) {
411                                         Composite middleware = new Composite(getContainer(), SWT.NONE);
412                                         GridLayoutFactory.fillDefaults().applyTo(middleware);
413
414                                         Control control = null;
415
416                                         // Create a wrapper for pageSite to make it possible
417                                         // for each tab contribution to specify its own
418                                         // selection provider.
419                                         final int tabIndex = index++;
420                                         IPageSite siteWrapper = new PageSiteProxy(pageSite) {
421                                             @Override
422                                             public void setSelectionProvider(ISelectionProvider provider) {
423                                                 tabSelectionProviders[tabIndex] = provider;
424                                                 if(pageSelectionProvider != null && provider != null)
425                                                         pageSelectionProvider.setAndFireNonEqualSelection(provider.getSelection());
426                                             }
427                                             @Override
428                                             public ISelectionProvider getSelectionProvider() {
429                                                 return tabSelectionProviders[tabIndex];
430                                             }
431                                         };
432
433                                         IPropertyTab tab = cc.create(middleware, siteWrapper, sessionContext, cc.getInput());
434                                         if (tab != null) {
435                                             tabs.add(tab);
436                                             control = tab.getControl();
437                                             if(control != null && !control.isDisposed()) GridDataFactory.fillDefaults().grab(true, true).applyTo(control);
438                                         } else {
439                                             control = createEmptyControl(middleware, SWT.NONE);
440 //                                            Button b = new Button(middleware, SWT.BORDER);
441 //                                            GridDataFactory.fillDefaults().grab(true, true).applyTo(b);
442 //                                            b.setText("FOO");
443                                         }
444
445                                         addPage(middleware, cc.getLabel(), cc.getImage());
446
447                                         if (tab != null) {
448                                             Object input = cc.getInput();
449                                             tab.setInput(sessionContext, new StructuredSelection(input), false);
450                                         }
451                                     }
452
453                                     ComparableTabContributor cc = TabbedPropertyTable.this.selectInitialTab(selectionContents, contributionList);
454                                     if (cc != null) {
455                                         int activePage = contributionList.indexOf(cc);
456                                         if (activePage != -1)
457                                             setActivePage(activePage);
458                                     }
459                                 }
460                             }
461
462                             Composite createEmptyControl(Composite parent, int flags) {
463                                 Composite empty = new Composite(parent, flags);
464                                 GridLayoutFactory.fillDefaults().applyTo(empty);
465                                 return empty;
466                             }
467                         });
468                         layout();
469                     }
470                 });
471             }
472         };
473     }
474
475     @Override
476     public Control getControl() {
477         return this;
478     }
479
480     @Override
481     public void requestFocus() {
482         IPropertyTab table = getActiveTab();
483         if (table != null)
484             table.requestFocus();
485     }
486
487     public IPropertyTab getActiveTab() {
488         int index = activeTab.get();
489         if (index < 0 || index >= tabs.size())
490             return null;
491         IPropertyTab tab = tabs.get(index);
492         return tab;
493     }
494
495     protected Composite createBaseComposite(final Composite parent, TabbedPropertyPage page) {
496         // 1. dispose the previous UI container
497         disposeAll();
498
499         // 2. construct new base for the tabbed property UI
500         baseComposite = new Composite(parent, SWT.NONE);
501
502         // <DEBUG>
503 //        parent.setBackground(getDisplay().getSystemColor(SWT.COLOR_RED));
504 //        baseComposite.setBackground(getDisplay().getSystemColor(SWT.COLOR_TITLE_INACTIVE_BACKGROUND));
505         // </DEBUG>
506
507         if (page != null)
508             page.createPartControl(baseComposite);
509
510         GridLayoutFactory.fillDefaults().applyTo(parent);
511         GridDataFactory.fillDefaults().grab(true, true).applyTo(baseComposite);
512         baseComposite.setLayout(new FillLayout());
513
514         return baseComposite;
515     }
516
517     private void disposeAll() {
518         if (baseComposite != null && !baseComposite.isDisposed()) {
519             baseComposite.dispose();
520             baseComposite = null;
521         }
522         activeTab.set(-1);
523         for (IPropertyTab tab : tabs)
524             tab.dispose();
525         tabs.clear();
526     }
527
528     @Override
529     public ISelectionProvider getSelectionProvider() {
530         return pageSelectionProvider;
531     }
532
533     /**
534      * Override to perform actions when the active tab changes.
535      *
536      * @param previousTab
537      * @param activeTab
538      */
539     protected void activeTabChanged(TabChangeEvent event) {
540     }
541
542     /**
543      * @param selectionContents 
544      * @param contributions
545      * @return
546      */
547     protected ComparableTabContributor selectInitialTab(Collection<Object> selectionContents, Collection<ComparableTabContributor> contributions) {
548         return null;
549     }
550
551     /**
552      * @return
553      */
554     protected int getTabFolderStyle() {
555         return SWT.NONE;
556     }
557
558 }