blob: f5238ebfee7cbff3329875cd53d0233ff650ef43 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2018 IBM Corporation and others.
* 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:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.equinox.internal.p2.ui.dialogs;
import java.util.*;
import org.eclipse.core.runtime.jobs.*;
import org.eclipse.equinox.internal.p2.ui.viewers.DeferredQueryContentProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.dialogs.FilteredTree;
import org.eclipse.ui.dialogs.PatternFilter;
import org.eclipse.ui.progress.WorkbenchJob;
/**
* FilteredTree extension that creates a ContainerCheckedTreeViewer, manages the
* check state across filtering (working around bugs in ContainerCheckedTreeViewer),
* and preloads all metadata repositories before allowing filtering, in order to
* coordinate background fetch and filtering. It also manages a cache of expanded
* elements that can survive a change of input.
*
* @since 3.4
*
*/
public class DelayedFilterCheckboxTree extends FilteredTree {
private static final long FILTER_DELAY = 800;
public static final Object ALL_ITEMS_HACK = new Object();
ToolBar toolBar;
Display display;
PatternFilter patternFilter;
IPreFilterJobProvider jobProvider;
DeferredQueryContentProvider contentProvider;
String savedFilterText;
Job preFilterJob;
WorkbenchJob filterJob;
boolean ignoreFiltering = true;
Object viewerInput;
HashSet<Object> checkState = new HashSet<>();
Set<Object> expanded = new HashSet<>();
ContainerCheckedTreeViewer checkboxViewer;
public DelayedFilterCheckboxTree(Composite parent, int treeStyle, PatternFilter filter, IPreFilterJobProvider jobProvider) {
super(parent, true);
this.display = parent.getDisplay();
this.patternFilter = filter;
init(treeStyle, filter);
}
@Override
protected TreeViewer doCreateTreeViewer(Composite composite, int style) {
checkboxViewer = new ContainerCheckedTreeViewer(composite, style);
checkboxViewer.addCheckStateListener(event -> {
// We use an additive check state cache so we need to remove
// previously checked items if the user unchecked them.
if (!event.getChecked() && checkState != null) {
if (event.getElement() == ALL_ITEMS_HACK) {
clearCheckStateCache();
} else {
ArrayList<Object> toRemove = new ArrayList<>(1);
// See bug 258117. Ideally we would get check state changes
// for children when the parent state changed, but we aren't, so
// we need to remove all children from the additive check state
// cache.
if (contentProvider.hasChildren(event.getElement())) {
Set<Object> unchecked = new HashSet<>();
// See bug 533655, We should uncheck all of the children
// of the triggering element, not just the direct descendants.
uncheckAllChildren(unchecked, event.getElement());
Iterator<Object> iter = checkState.iterator();
while (iter.hasNext()) {
Object current = iter.next();
if (current != null && unchecked.contains(current)) {
toRemove.add(current);
}
}
} else {
for (Object element2 : checkState) {
if (checkboxViewer.getComparer().equals(element2, event.getElement())) {
toRemove.add(element2);
// Do not break out of the loop. We may have duplicate equal
// elements in the cache. Since the cache is additive, we want
// to be sure we've gotten everything.
}
}
}
checkState.removeAll(toRemove);
}
} else if (event.getChecked()) {
rememberLeafCheckState();
}
});
return checkboxViewer;
}
private void uncheckAllChildren(Set<Object> unchecked, Object element) {
for (Object child : contentProvider.getChildren(element)) {
unchecked.add(child);
if (contentProvider.getChildren(child).length > 0) {
uncheckAllChildren(unchecked, child);
}
}
}
@Override
protected Composite createFilterControls(Composite filterParent) {
super.createFilterControls(filterParent);
filterParent.addDisposeListener(e -> cancelPreFilterJob());
return filterParent;
}
public void contentProviderSet(final DeferredQueryContentProvider deferredProvider) {
this.contentProvider = deferredProvider;
deferredProvider.addListener((v, oldInput, newInput) -> {
if (newInput == null) {
return;
}
// Store the input because it's not reset in the viewer until
// after this listener is run.
viewerInput = newInput;
// If we were loading repos, we want to cancel because there may be more.
cancelPreFilterJob();
// Cancel any filtering
cancelAndResetFilterJob();
contentProvider.setSynchronous(false);
// Remember any previous expansions
rememberExpansions();
// If there are remembered check states, try to restore them.
// Must be done in an async because we are in the middle of a buggy
// selection preserving viewer refresh.
checkboxViewer.getTree().setRedraw(false);
display.asyncExec(() -> {
if (checkboxViewer.getTree().isDisposed()) {
return;
}
rememberExpansions();
restoreLeafCheckState();
rememberExpansions();
restoreExpansions();
checkboxViewer.getTree().setRedraw(true);
});
});
}
public void clearCheckStateCache() {
checkState = null;
}
/*
* Overridden to hook a listener on the job and set the deferred content provider
* to synchronous mode before a filter is done.
* @see org.eclipse.ui.dialogs.FilteredTree#doCreateRefreshJob()
*/
@Override
protected WorkbenchJob doCreateRefreshJob() {
filterJob = super.doCreateRefreshJob();
filterJob.addJobChangeListener(new JobChangeAdapter() {
@Override
public void aboutToRun(final IJobChangeEvent event) {
// If we know we've already filtered and loaded repos, nothing more to do
if (!ignoreFiltering) {
return;
}
final boolean[] shouldPreFilter = new boolean[1];
shouldPreFilter[0] = false;
display.syncExec(() -> {
if (filterText != null && !filterText.isDisposed()) {
String text = getFilterString();
// If we are about to filter and there is
// actually filtering to do, check for a prefilter
// job and the content provider to synchronous mode.
// We want the prefilter job to complete before continuing with filtering.
if (text == null || (initialText != null && initialText.equals(text))) {
return;
}
if (!contentProvider.getSynchronous() && preFilterJob == null) {
if (filterText != null && !filterText.isDisposed()) {
shouldPreFilter[0] = true;
}
}
}
});
if (shouldPreFilter[0]) {
event.getJob().sleep();
schedulePreFilterJob();
} else if (ignoreFiltering) {
event.getJob().sleep();
} else {
// shouldn't get here unless the prefilter job finished
// and ignoreFiltering became false since we entered this listener.
rememberLeafCheckState();
}
}
@Override
public void running(IJobChangeEvent event) {
display.syncExec(() -> rememberLeafCheckState());
}
@Override
public void done(IJobChangeEvent event) {
if (event.getResult().isOK()) {
display.asyncExec(() -> {
if (checkboxViewer.getTree().isDisposed()) {
return;
}
checkboxViewer.getTree().setRedraw(false);
// remember things expanded by the filter
rememberExpansions();
restoreLeafCheckState();
// now restore expansions because we may have
// had others
restoreExpansions();
checkboxViewer.getTree().setRedraw(true);
});
}
}
});
return filterJob;
}
void schedulePreFilterJob() {
// cancel any existing jobs
cancelPreFilterJob();
ignoreFiltering = false;
preFilterJob = jobProvider == null ? null : jobProvider.getPreFilterJob();
if (preFilterJob == null) {
if (filterJob != null) {
filterJob.wakeUp();
}
return;
}
ignoreFiltering = true;
preFilterJob.addJobChangeListener(new JobChangeAdapter() {
@Override
public void done(IJobChangeEvent event) {
ignoreFiltering = false;
contentProvider.setSynchronous(true);
if (filterJob != null) {
filterJob.wakeUp();
}
preFilterJob = null;
}
});
preFilterJob.setSystem(true);
preFilterJob.setUser(false);
preFilterJob.schedule();
}
void cancelPreFilterJob() {
if (preFilterJob != null) {
preFilterJob.cancel();
preFilterJob = null;
}
}
void cancelAndResetFilterJob() {
if (filterJob != null) {
filterJob.cancel();
}
}
void rememberLeafCheckState() {
ContainerCheckedTreeViewer v = (ContainerCheckedTreeViewer) getViewer();
Object[] checked = v.getCheckedElements();
if (checkState == null) {
checkState = new HashSet<>(checked.length);
}
for (int i = 0; i < checked.length; i++) {
if (!v.getGrayed(checked[i]) && contentProvider.getChildren(checked[i]).length == 0) {
if (!checkState.contains(checked[i])) {
checkState.add(checked[i]);
}
}
}
}
void restoreLeafCheckState() {
if (checkboxViewer == null || checkboxViewer.getTree().isDisposed()) {
return;
}
if (checkState == null) {
return;
}
checkboxViewer.setCheckedElements(new Object[0]);
checkboxViewer.setGrayedElements(new Object[0]);
// Now we are only going to set the check state of the leaf nodes
// and rely on our container checked code to update the parents properly.
Iterator<Object> iter = checkState.iterator();
Object element = null;
if (iter.hasNext()) {
checkboxViewer.expandAll();
}
while (iter.hasNext()) {
element = iter.next();
checkboxViewer.setChecked(element, true);
}
// We are only firing one event, knowing that this is enough for our listeners.
if (element != null) {
checkboxViewer.fireCheckStateChanged(element, true);
}
}
void rememberExpansions() {
// The expansions are additive, but we are using a set to keep out
// duplicates. In practice, this means expanded items from different
// inputs will remain expanded, such as categories with the same name
// in different repos.
expanded.addAll(Arrays.asList(getViewer().getExpandedElements()));
}
void restoreExpansions() {
getViewer().setExpandedElements(expanded.toArray());
}
public ContainerCheckedTreeViewer getCheckboxTreeViewer() {
return checkboxViewer;
}
@Override
protected long getRefreshJobDelay() {
return FILTER_DELAY;
}
public Object[] getCheckedElements() {
if (this.checkState != null) {
return this.checkState.toArray();
}
return new Object[0];
}
@Override
protected String getFilterString() {
return super.getFilterString();
}
}