/*******************************************************************************
 * Copyright (c) 2000, 2016 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.dialogs;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MenuAdapter;
import org.eclipse.swt.events.MenuEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.ProgressMonitorWrapper;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;

import org.eclipse.jface.resource.ImageDescriptor;

import org.eclipse.ui.progress.UIJob;

import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.WorkingCopyOwner;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.TypeNameMatch;
import org.eclipse.jdt.core.search.TypeNameMatchRequestor;
import org.eclipse.jdt.core.search.TypeNameRequestor;

import org.eclipse.jdt.internal.core.manipulation.util.Strings;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.internal.corext.util.OpenTypeHistory;
import org.eclipse.jdt.internal.corext.util.TypeFilter;
import org.eclipse.jdt.internal.corext.util.TypeInfoFilter;
import org.eclipse.jdt.internal.corext.util.TypeInfoRequestorAdapter;

import org.eclipse.jdt.launching.IVMInstall;
import org.eclipse.jdt.launching.IVMInstallType;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jdt.launching.LibraryLocation;

import org.eclipse.jdt.ui.JavaElementLabels;
import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension;
import org.eclipse.jdt.ui.dialogs.ITypeInfoImageProvider;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.JavaPluginImages;
import org.eclipse.jdt.internal.ui.JavaUIMessages;
import org.eclipse.jdt.internal.ui.viewsupport.JavaElementImageProvider;


/**
 * DO NOT REMOVE, used in a product.
 * @deprecated As of 3.5, replaced by {@link org.eclipse.ui.dialogs.FilteredItemsSelectionDialog}
 */
@Deprecated
@SuppressWarnings("all")
public class TypeInfoViewer {

	private static class SearchRequestor extends TypeNameMatchRequestor {
		private volatile boolean fStop;

		private Set fHistory;

		private TypeInfoFilter fFilter;
		private List fResult;

		public SearchRequestor(TypeInfoFilter filter) {
			super();
			fResult= new ArrayList(2048);
			fFilter= filter;
		}
		public TypeNameMatch[] getResult() {
			return (TypeNameMatch[])fResult.toArray(new TypeNameMatch[fResult.size()]);
		}
		public void cancel() {
			fStop= true;
		}
		public void setHistory(Set history) {
			fHistory= history;
		}

		@Override
		public void acceptTypeNameMatch(TypeNameMatch match) {
			if (fStop)
				return;
			if (TypeFilter.isFiltered(match))
				return;
			if (fHistory.contains(match))
				return;
			if (fFilter.matchesFilterExtension(match))
				fResult.add(match);
		}
	}

	protected static class TypeInfoComparator implements Comparator {
		private TypeInfoLabelProvider fLabelProvider;
		private TypeInfoFilter fFilter;
		public TypeInfoComparator(TypeInfoLabelProvider labelProvider, TypeInfoFilter filter) {
			fLabelProvider= labelProvider;
			fFilter= filter;
		}
	    @Override
		public int compare(Object left, Object right) {
	    	TypeNameMatch leftInfo= (TypeNameMatch)left;
	    	TypeNameMatch rightInfo= (TypeNameMatch)right;
	     	int leftCategory= getCamelCaseCategory(leftInfo);
	     	int rightCategory= getCamelCaseCategory(rightInfo);
	     	if (leftCategory < rightCategory)
	     		return -1;
	     	if (leftCategory > rightCategory)
	     		return +1;
	     	int result= compareName(leftInfo.getSimpleTypeName(), rightInfo.getSimpleTypeName());
	     	if (result != 0)
	     		return result;
	     	result= compareTypeContainerName(leftInfo.getTypeContainerName(), rightInfo.getTypeContainerName());
	     	if (result != 0)
	     		return result;

	     	leftCategory= getElementTypeCategory(leftInfo);
	     	rightCategory= getElementTypeCategory(rightInfo);
	     	if (leftCategory < rightCategory)
	     		return -1;
	     	if (leftCategory > rightCategory)
	     		return +1;
	     	return compareContainerName(leftInfo, rightInfo);
	    }
		private int compareName(String leftString, String rightString) {
			int result= leftString.compareToIgnoreCase(rightString);
			if (result != 0 || rightString.length() == 0) {
				return result;
			} else if (Strings.isLowerCase(leftString.charAt(0)) &&
				!Strings.isLowerCase(rightString.charAt(0))) {
	     		return +1;
			} else if (Strings.isLowerCase(rightString.charAt(0)) &&
	     		!Strings.isLowerCase(leftString.charAt(0))) {
	     		return -1;
			} else {
				return leftString.compareTo(rightString);
			}
		}
		private int compareTypeContainerName(String leftString, String rightString) {
			int leftLength= leftString.length();
			int rightLength= rightString.length();
			if (leftLength == 0 && rightLength > 0)
				return -1;
			if (leftLength == 0 && rightLength == 0)
				return 0;
			if (leftLength > 0 && rightLength == 0)
				return +1;
			return compareName(leftString, rightString);
		}
		private int compareContainerName(TypeNameMatch leftType, TypeNameMatch rightType) {
			return fLabelProvider.getContainerName(leftType).compareTo(
				fLabelProvider.getContainerName(rightType));
		}
		private int getCamelCaseCategory(TypeNameMatch type) {
			if (fFilter == null)
				return 0;
			if (!fFilter.isCamelCasePattern())
				return 0;
			return fFilter.matchesRawNamePattern(type) ? 0 : 1;
		}
		private int getElementTypeCategory(TypeNameMatch type) {
			try {
				if (type.getPackageFragmentRoot().getKind() == IPackageFragmentRoot.K_SOURCE)
					return 0;
			} catch (JavaModelException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			return 1;
		}
	}

	protected static class TypeInfoLabelProvider {

		private ITypeInfoImageProvider fProviderExtension;
		private TypeInfoRequestorAdapter fAdapter= new TypeInfoRequestorAdapter();

		private Map fLib2Name= new HashMap();
		private String[] fInstallLocations;
		private String[] fVMNames;

		private boolean fFullyQualifyDuplicates;

		public TypeInfoLabelProvider(ITypeInfoImageProvider extension) {
			fProviderExtension= extension;
			List locations= new ArrayList();
			List labels= new ArrayList();
			for (IVMInstallType install : JavaRuntime.getVMInstallTypes()) {
				processVMInstallType(install, locations, labels);
			}
			fInstallLocations= (String[])locations.toArray(new String[locations.size()]);
			fVMNames= (String[])labels.toArray(new String[labels.size()]);

		}
		public void setFullyQualifyDuplicates(boolean value) {
			fFullyQualifyDuplicates= value;
		}
		private void processVMInstallType(IVMInstallType installType, List locations, List labels) {
			if (installType != null) {
				boolean isMac= Platform.OS_MACOSX.equals(Platform.getOS());
				final String HOME_SUFFIX= "/Home"; //$NON-NLS-1$
				for (IVMInstall install : installType.getVMInstalls()) {
					String label= getFormattedLabel(install.getName());
					LibraryLocation[] libLocations= install.getLibraryLocations();
					if (libLocations != null) {
						processLibraryLocation(libLocations, label);
					} else {
						String filePath= install.getInstallLocation().getAbsolutePath();
						// on MacOS X install locations end in an additional "/Home" segment; remove it
						if (isMac && filePath.endsWith(HOME_SUFFIX))
							filePath= filePath.substring(0, filePath.length()- HOME_SUFFIX.length() + 1);
						locations.add(filePath);
						labels.add(label);
					}
				}
			}
		}
		private void processLibraryLocation(LibraryLocation[] libLocations, String label) {
			for (LibraryLocation location : libLocations) {
				fLib2Name.put(location.getSystemLibraryPath().toString(), label);
			}
		}
		private String getFormattedLabel(String name) {
			return Messages.format(JavaUIMessages.TypeInfoViewer_library_name_format, name);
		}
		public String getText(Object element) {
			return ((TypeNameMatch)element).getSimpleTypeName();
		}
		public String getQualifiedText(TypeNameMatch type) {
			StringBuilder result= new StringBuilder();
			result.append(type.getSimpleTypeName());
			String containerName= type.getTypeContainerName();
			result.append(JavaElementLabels.CONCAT_STRING);
			if (containerName.length() > 0) {
				result.append(containerName);
			} else {
				result.append(JavaUIMessages.TypeInfoViewer_default_package);
			}
			return result.toString();
		}
		public String getFullyQualifiedText(TypeNameMatch type) {
			StringBuilder result= new StringBuilder();
			result.append(type.getSimpleTypeName());
			String containerName= type.getTypeContainerName();
			if (containerName.length() > 0) {
				result.append(JavaElementLabels.CONCAT_STRING);
				result.append(containerName);
			}
			result.append(JavaElementLabels.CONCAT_STRING);
			result.append(getContainerName(type));
			return result.toString();
		}
		public String getText(TypeNameMatch last, TypeNameMatch current, TypeNameMatch next) {
			StringBuilder result= new StringBuilder();
			int qualifications= 0;
			String currentTN= current.getSimpleTypeName();
			result.append(currentTN);
			String currentTCN= getTypeContainerName(current);
			if (last != null) {
				String lastTN= last.getSimpleTypeName();
				String lastTCN= getTypeContainerName(last);
				if (currentTCN.equals(lastTCN)) {
					if (currentTN.equals(lastTN)) {
						result.append(JavaElementLabels.CONCAT_STRING);
						result.append(currentTCN);
						result.append(JavaElementLabels.CONCAT_STRING);
						result.append(getContainerName(current));
						return result.toString();
					}
				} else if (currentTN.equals(lastTN)) {
					qualifications= 1;
				}
			}
			if (next != null) {
				String nextTN= next.getSimpleTypeName();
				String nextTCN= getTypeContainerName(next);
				if (currentTCN.equals(nextTCN)) {
					if (currentTN.equals(nextTN)) {
						result.append(JavaElementLabels.CONCAT_STRING);
						result.append(currentTCN);
						result.append(JavaElementLabels.CONCAT_STRING);
						result.append(getContainerName(current));
						return result.toString();
					}
				} else if (currentTN.equals(nextTN)) {
					qualifications= 1;
				}
			}
			if (qualifications > 0) {
				result.append(JavaElementLabels.CONCAT_STRING);
				result.append(currentTCN);
				if (fFullyQualifyDuplicates) {
					result.append(JavaElementLabels.CONCAT_STRING);
					result.append(getContainerName(current));
				}
			}
			return result.toString();
		}
		public String getQualificationText(TypeNameMatch type) {
			StringBuilder result= new StringBuilder();
			String containerName= type.getTypeContainerName();
			if (containerName.length() > 0) {
				result.append(containerName);
				result.append(JavaElementLabels.CONCAT_STRING);
			}
			result.append(getContainerName(type));
			return result.toString();
		}

		private boolean isInnerType(TypeNameMatch match) {
			return match.getTypeQualifiedName().indexOf('.') != -1;
		}

		public ImageDescriptor getImageDescriptor(Object element) {
			TypeNameMatch type= (TypeNameMatch)element;
			if (fProviderExtension != null) {
				fAdapter.setMatch(type);
				ImageDescriptor descriptor= fProviderExtension.getImageDescriptor(fAdapter);
				if (descriptor != null)
					return descriptor;
			}
			return JavaElementImageProvider.getTypeImageDescriptor(
				isInnerType(type), false, type.getModifiers(), false);
		}

		private String getTypeContainerName(TypeNameMatch info) {
			String result= info.getTypeContainerName();
			if (result.length() > 0)
				return result;
			return JavaUIMessages.TypeInfoViewer_default_package;
		}

		private String getContainerName(TypeNameMatch type) {
			IPackageFragmentRoot root= type.getPackageFragmentRoot();
			if (root.isExternal()) {
				String name= root.getPath().toOSString();
				for (int i= 0; i < fInstallLocations.length; i++) {
					if (name.startsWith(fInstallLocations[i])) {
						return fVMNames[i];
					}
				}
				String lib= (String)fLib2Name.get(name);
				if (lib != null)
					return lib;
			}
			StringBuffer buf= new StringBuffer();
			JavaElementLabels.getPackageFragmentRootLabel(root, JavaElementLabels.ROOT_QUALIFIED | JavaElementLabels.ROOT_VARIABLE, buf);
			return buf.toString();
		}
	}

	private static class ProgressUpdateJob extends UIJob {
		private TypeInfoViewer fViewer;
		private boolean fStopped;
		public ProgressUpdateJob(Display display, TypeInfoViewer viewer) {
			super(display, JavaUIMessages.TypeInfoViewer_progressJob_label);
			fViewer= viewer;
		}
		public void stop() {
			fStopped= true;
			cancel();
		}
		@Override
		public IStatus runInUIThread(IProgressMonitor monitor) {
			if (stopped())
				return new Status(IStatus.CANCEL, JavaPlugin.getPluginId(), IStatus.CANCEL, "", null); //$NON-NLS-1$
			fViewer.updateProgressMessage();
			if (!stopped())
				schedule(300);
			return new Status(IStatus.OK, JavaPlugin.getPluginId(), IStatus.OK, "", null); //$NON-NLS-1$
		}
		private boolean stopped() {
			return fStopped || fViewer.getTable().isDisposed();
		}
	}

	private static class ProgressMonitor extends ProgressMonitorWrapper {
		private TypeInfoViewer fViewer;
		private String fName;
		private int fTotalWork;
		private double fWorked;
		private boolean fDone;

		public ProgressMonitor(IProgressMonitor monitor, TypeInfoViewer viewer) {
			super(monitor);
			fViewer= viewer;
		}
		@Override
		public void setTaskName(String name) {
			super.setTaskName(name);
			fName= name;
		}
		@Override
		public void beginTask(String name, int totalWork) {
			super.beginTask(name, totalWork);
			if (fName == null)
				fName= name;
			fTotalWork= totalWork;
		}
		@Override
		public void worked(int work) {
			super.worked(work);
			internalWorked(work);
		}
		@Override
		public void done() {
			fDone= true;
			fViewer.setProgressMessage(""); //$NON-NLS-1$
			super.done();
		}
		@Override
		public void internalWorked(double work) {
			fWorked= fWorked + work;
			fViewer.setProgressMessage(getMessage());
		}
		private String getMessage() {
			if (fDone) {
				return ""; //$NON-NLS-1$
			} else if (fTotalWork == 0) {
				return fName;
			} else {
				return Messages.format(
					JavaUIMessages.TypeInfoViewer_progress_label,
					new Object[] { fName, Integer.valueOf((int)((fWorked * 100) / fTotalWork)) });
			}
		}
	}

	private static abstract class AbstractJob extends Job {
		protected TypeInfoViewer fViewer;
		protected AbstractJob(String name, TypeInfoViewer viewer) {
			super(name);
			fViewer= viewer;
			setSystem(true);
		}
		@Override
		protected final IStatus run(IProgressMonitor parent) {
			ProgressMonitor monitor= new ProgressMonitor(parent, fViewer);
			try {
				fViewer.scheduleProgressUpdateJob();
				return doRun(monitor);
			} finally {
				fViewer.stopProgressUpdateJob();
			}
		}
		protected abstract IStatus doRun(ProgressMonitor monitor);
	}

	private static abstract class AbstractSearchJob extends AbstractJob {
		private int fMode;

		protected int fTicket;
		protected TypeInfoLabelProvider fLabelProvider;

		protected TypeInfoFilter fFilter;
		protected OpenTypeHistory fHistory;

		protected AbstractSearchJob(int ticket, TypeInfoViewer viewer, TypeInfoFilter filter, OpenTypeHistory history, int numberOfVisibleItems, int mode) {
			super(JavaUIMessages.TypeInfoViewer_job_label, viewer);
			fMode= mode;
			fTicket= ticket;
			fViewer= viewer;
			fLabelProvider= fViewer.getLabelProvider();
			fFilter= filter;
			fHistory= history;
		}
		public void stop() {
			cancel();
		}
		@Override
		protected IStatus doRun(ProgressMonitor monitor) {
			try {
				if (VIRTUAL) {
					internalRunVirtual(monitor);
				} else {
					internalRun(monitor);
				}
			} catch (CoreException e) {
				fViewer.searchJobFailed(fTicket, e);
				return new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IStatus.ERROR, JavaUIMessages.TypeInfoViewer_job_error, e);
			} catch (InterruptedException e) {
				return canceled(e, true);
			} catch (OperationCanceledException e) {
				return canceled(e, false);
			}
			fViewer.searchJobDone(fTicket);
			return ok();
		}
		protected abstract TypeNameMatch[] getSearchResult(Set matchIdsInHistory, ProgressMonitor monitor) throws CoreException;

		private void internalRun(ProgressMonitor monitor) throws CoreException, InterruptedException {
			if (monitor.isCanceled())
				throw new OperationCanceledException();

			fViewer.clear(fTicket);

			// local vars to speed up rendering
			TypeNameMatch last= null;
			TypeNameMatch type= null;
			TypeNameMatch next= null;
			List elements= new ArrayList();
			List imageDescriptors= new ArrayList();
			List labels= new ArrayList();
			Set filteredMatches= new HashSet();

			TypeNameMatch[] matchingTypes= fHistory.getFilteredTypeInfos(fFilter);
			if (matchingTypes.length > 0) {
				Arrays.sort(matchingTypes, new TypeInfoComparator(fLabelProvider, fFilter));
				type= matchingTypes[0];
				int i= 1;
				while(type != null) {
					next= (i == matchingTypes.length) ? null : matchingTypes[i];
					elements.add(type);
					filteredMatches.add(type);
					imageDescriptors.add(fLabelProvider.getImageDescriptor(type));
					labels.add(fLabelProvider.getText(last, type, next));
					last= type;
					type= next;
					i++;
				}
			}
			matchingTypes= null;
			fViewer.fExpectedItemCount= elements.size();
			fViewer.addHistory(fTicket, elements, imageDescriptors, labels);

			if ((fMode & INDEX) == 0) {
				return;
			}
			TypeNameMatch[] result= getSearchResult(filteredMatches, monitor);
			fViewer.fExpectedItemCount+= result.length;
			if (result.length == 0) {
				return;
			}
			if (monitor.isCanceled())
				throw new OperationCanceledException();
			int processed= 0;
			int nextIndex= 1;
			type= result[0];
			if (!filteredMatches.isEmpty()) {
				fViewer.addDashLineAndUpdateLastHistoryEntry(fTicket, type);
			}
			while (true) {
				long startTime= System.currentTimeMillis();
				elements.clear();
				imageDescriptors.clear();
				labels.clear();
	            int delta = Math.min(nextIndex == 1 ? fViewer.getNumberOfVisibleItems() : 10, result.length - processed);
				if (delta == 0)
					break;
				processed= processed + delta;
				while(delta > 0) {
					next= (nextIndex == result.length) ? null : result[nextIndex];
					elements.add(type);
					labels.add(fLabelProvider.getText(last, type, next));
					imageDescriptors.add(fLabelProvider.getImageDescriptor(type));
					last= type;
					type= next;
					nextIndex++;
					delta--;
				}
				fViewer.addAll(fTicket, elements, imageDescriptors, labels);
				long sleep= 100 - (System.currentTimeMillis() - startTime);

				if (sleep > 0)
					Thread.sleep(sleep);

				if (monitor.isCanceled())
					throw new OperationCanceledException();
			}
		}
		private void internalRunVirtual(ProgressMonitor monitor) throws CoreException, InterruptedException {
			if (monitor.isCanceled())
				throw new OperationCanceledException();

			fViewer.clear(fTicket);

			TypeNameMatch[] matchingTypes= fHistory.getFilteredTypeInfos(fFilter);
			fViewer.setHistoryResult(fTicket, matchingTypes);
			if ((fMode & INDEX) == 0)
				return;

			Set filteredMatches= new HashSet(matchingTypes.length * 2);
			filteredMatches.addAll(Arrays.asList(matchingTypes));

			TypeNameMatch[] result= getSearchResult(filteredMatches, monitor);
			if (monitor.isCanceled())
				throw new OperationCanceledException();

			fViewer.setSearchResult(fTicket, result);
		}
		private IStatus canceled(Exception e, boolean removePendingItems) {
			fViewer.searchJobCanceled(fTicket, removePendingItems);
			return new Status(IStatus.CANCEL, JavaPlugin.getPluginId(), IStatus.CANCEL, JavaUIMessages.TypeInfoViewer_job_cancel, e);
		}
		private IStatus ok() {
			return new Status(IStatus.OK, JavaPlugin.getPluginId(), IStatus.OK, "", null); //$NON-NLS-1$
		}
	}

	private static class SearchEngineJob extends AbstractSearchJob {
		private IJavaSearchScope fScope;
		private int fElementKind;
		private SearchRequestor fReqestor;

		public SearchEngineJob(int ticket, TypeInfoViewer viewer, TypeInfoFilter filter, OpenTypeHistory history, int numberOfVisibleItems, int mode,
				IJavaSearchScope scope, int elementKind) {
			super(ticket, viewer, filter, history, numberOfVisibleItems, mode);
			fScope= scope;
			fElementKind= elementKind;
			fReqestor= new SearchRequestor(filter);
		}
		@Override
		public void stop() {
			fReqestor.cancel();
			super.stop();
		}
		@Override
		protected TypeNameMatch[] getSearchResult(Set matchIdsInHistory, ProgressMonitor monitor) throws CoreException {
			long start= System.currentTimeMillis();
			fReqestor.setHistory(matchIdsInHistory);
			// consider primary working copies during searching
			SearchEngine engine= new SearchEngine((WorkingCopyOwner)null);
			String packPattern= fFilter.getPackagePattern();
			monitor.setTaskName(JavaUIMessages.TypeInfoViewer_searchJob_taskName);
			engine.searchAllTypeNames(
				packPattern == null ? null : packPattern.toCharArray(),
				fFilter.getPackageFlags(),
				fFilter.getNamePattern().toCharArray(),
				fFilter.getSearchFlags(),
				fElementKind,
				fScope,
				fReqestor,
				IJavaSearchConstants.WAIT_UNTIL_READY_TO_SEARCH,
				monitor);
			if (DEBUG)
				System.out.println("Time needed until search has finished: " + (System.currentTimeMillis() - start)); //$NON-NLS-1$
			TypeNameMatch[] result= fReqestor.getResult();
			Arrays.sort(result, new TypeInfoComparator(fLabelProvider, fFilter));
			if (DEBUG)
				System.out.println("Time needed until sort has finished: " + (System.currentTimeMillis() - start)); //$NON-NLS-1$
			fViewer.rememberResult(fTicket, result);
			return result;
		}
	}

	private static class CachedResultJob extends AbstractSearchJob {
		private TypeNameMatch[] fLastResult;
		public CachedResultJob(int ticket, TypeNameMatch[] lastResult, TypeInfoViewer viewer, TypeInfoFilter filter, OpenTypeHistory history, int numberOfVisibleItems, int mode) {
			super(ticket, viewer, filter, history, numberOfVisibleItems, mode);
			fLastResult= lastResult;
		}
		@Override
		protected TypeNameMatch[] getSearchResult(Set filteredHistory, ProgressMonitor monitor) throws CoreException {
			List result= new ArrayList(2048);
			for (TypeNameMatch type : fLastResult) {
				if (filteredHistory.contains(type))
					continue;
				if (fFilter.matchesCachedResult(type))
					result.add(type);
			}
			// we have to sort if the filter is a camel case filter.
			TypeNameMatch[] types= (TypeNameMatch[])result.toArray(new TypeNameMatch[result.size()]);
			if (fFilter.isCamelCasePattern()) {
				Arrays.sort(types, new TypeInfoComparator(fLabelProvider, fFilter));
			}
			return types;
		}
	}

	private static class SyncJob extends AbstractJob {
		public SyncJob(TypeInfoViewer viewer) {
			super(JavaUIMessages.TypeInfoViewer_syncJob_label, viewer);
		}
		public void stop() {
			cancel();
		}
		@Override
		protected IStatus doRun(ProgressMonitor monitor) {
			try {
				monitor.setTaskName(JavaUIMessages.TypeInfoViewer_syncJob_taskName);
				new SearchEngine().searchAllTypeNames(
					null,
					0,
					// make sure we search a concrete name. This is faster according to Kent
					"_______________".toCharArray(), //$NON-NLS-1$
					SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE,
					IJavaSearchConstants.ENUM,
					SearchEngine.createWorkspaceScope(),
					new TypeNameRequestor() {},
					IJavaSearchConstants.WAIT_UNTIL_READY_TO_SEARCH,
					monitor);
			} catch (JavaModelException e) {
				JavaPlugin.log(e);
				return new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IStatus.ERROR, JavaUIMessages.TypeInfoViewer_job_error, e);
			} catch (OperationCanceledException e) {
				return new Status(IStatus.CANCEL, JavaPlugin.getPluginId(), IStatus.CANCEL, JavaUIMessages.TypeInfoViewer_job_cancel, e);
			} finally {
				fViewer.syncJobDone();
			}
			return new Status(IStatus.OK, JavaPlugin.getPluginId(), IStatus.OK, "", null); //$NON-NLS-1$
		}
	}

	private static class DashLine {
		private int fSeparatorWidth;
		private String fMessage;
		private int fMessageLength;
		public String getText(int width) {
			StringBuilder dashes= new StringBuilder();
			int chars= (((width - fMessageLength) / fSeparatorWidth) / 2) -2;
			for (int i= 0; i < chars; i++) {
				dashes.append(SEPARATOR);
			}
			StringBuilder result= new StringBuilder();
			result.append(dashes);
			result.append(fMessage);
			result.append(dashes);
			return result.toString();
		}
		public void initialize(GC gc) {
			fSeparatorWidth= gc.getAdvanceWidth(SEPARATOR);
			fMessage= " " + JavaUIMessages.TypeInfoViewer_separator_message + " ";  //$NON-NLS-1$ //$NON-NLS-2$
			fMessageLength= gc.textExtent(fMessage).x;
		}
	}

	private static class ImageManager {
		private Map fImages= new HashMap(20);

		public Image get(ImageDescriptor descriptor) {
			if (descriptor == null)
				descriptor= ImageDescriptor.getMissingImageDescriptor();

			Image result= (Image)fImages.get(descriptor);
			if (result != null)
				return result;
			result= descriptor.createImage();
			if (result != null)
				fImages.put(descriptor, result);
			return result;
		}

		public void dispose() {
			for (Object element : fImages.values()) {
				Image image= (Image)element;
				image.dispose();
			}
			fImages.clear();
		}
	}

	private Display fDisplay;

	private String fProgressMessage;
	private Label fProgressLabel;
	private int fProgressCounter;
	private ProgressUpdateJob fProgressUpdateJob;

	private OpenTypeHistory fHistory;

	/* non virtual table */
	private int fNextElement;
	private List fItems;

	/* virtual table */
	private TypeNameMatch[] fHistoryMatches;
	private TypeNameMatch[] fSearchMatches;

	private int fNumberOfVisibleItems;
	private int fExpectedItemCount;
	private Color fDashLineColor;
	private int fScrollbarWidth;
	private int fTableWidthDelta;
	private int fDashLineIndex= -1;
	private Image fSeparatorIcon;
	private DashLine fDashLine= new DashLine();

	private boolean fFullyQualifySelection;
	/* remembers the last selection to restore unqualified labels */
	private TableItem[] fLastSelection;
	private String[] fLastLabels;

	private TypeInfoLabelProvider fLabelProvider;
	private ImageManager fImageManager;

	private Table fTable;

	private SyncJob fSyncJob;

	private TypeInfoFilter fTypeInfoFilter;
	private ITypeInfoFilterExtension fFilterExtension;
	private TypeNameMatch[] fLastCompletedResult;
	private TypeInfoFilter fLastCompletedFilter;

	private int fSearchJobTicket;
	protected int fElementKind;
	protected IJavaSearchScope fSearchScope;

	private AbstractSearchJob fSearchJob;

	private static final int HISTORY= 1;
	private static final int INDEX= 2;
	private static final int FULL= HISTORY | INDEX;

	private static final char SEPARATOR= '-';

	private static final boolean DEBUG= false;
	private static final boolean VIRTUAL= false;

	private static final TypeNameMatch[] EMTPY_TYPE_INFO_ARRAY= new TypeNameMatch[0];
	// only needed when in virtual table mode

	private static final TypeNameMatch DASH_LINE= SearchEngine.createTypeNameMatch(null, 0);


	public TypeInfoViewer(Composite parent, int flags, Label progressLabel,
			IJavaSearchScope scope, int elementKind, String initialFilter,
			ITypeInfoFilterExtension filterExtension, ITypeInfoImageProvider imageExtension) {
		Assert.isNotNull(scope);
		fDisplay= parent.getDisplay();
		fProgressLabel= progressLabel;
		fSearchScope= scope;
		fElementKind= elementKind;
		fFilterExtension= filterExtension;
		fFullyQualifySelection= (flags & SWT.MULTI) != 0;
		if (VIRTUAL)
			flags|= SWT.VIRTUAL;
		fTable= new Table(parent, SWT.V_SCROLL | SWT.H_SCROLL | SWT.BORDER | SWT.FLAT | flags);
		fTable.setFont(parent.getFont());
		fLabelProvider= new TypeInfoLabelProvider(imageExtension);
		fItems= new ArrayList(500);
		fTable.setHeaderVisible(false);
		addPopupMenu();
		fTable.addControlListener(new ControlAdapter() {
			@Override
			public void controlResized(ControlEvent event) {
				int itemHeight= fTable.getItemHeight();
				Rectangle clientArea= fTable.getClientArea();
				fNumberOfVisibleItems= (clientArea.height / itemHeight) + 1;
			}
		});
		fTable.addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {
				if (e.keyCode == SWT.DEL) {
					deleteHistoryEntry();
				} else if (e.keyCode == SWT.ARROW_DOWN) {
					int index= fTable.getSelectionIndex();
					if (index == fDashLineIndex - 1) {
						e.doit= false;
						setTableSelection(index + 2);
					}
				} else if (e.keyCode == SWT.ARROW_UP) {
					int index= fTable.getSelectionIndex();
					if (fDashLineIndex != -1 && index == fDashLineIndex + 1) {
						e.doit= false;
						setTableSelection(index - 2);
					}
				}
			}
		});
		fTable.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				if (fLastSelection != null) {
					for (int i= 0; i < fLastSelection.length; i++) {
						TableItem item= fLastSelection[i];
						// could be disposed by deleting element from
						// type info history
						if (!item.isDisposed())
							item.setText(fLastLabels[i]);
					}
				}
				TableItem[] items= fTable.getSelection();
				fLastSelection= new TableItem[items.length];
				fLastLabels= new String[items.length];
				for (int i= 0; i < items.length; i++) {
					TableItem item= items[i];
					fLastSelection[i]= item;
					fLastLabels[i]= item.getText();
					Object data= item.getData();
					if (data instanceof TypeNameMatch) {
						String qualifiedText= getQualifiedText((TypeNameMatch)data);
						if (qualifiedText.length() > fLastLabels[i].length())
							item.setText(qualifiedText);
					}
				}
			}
		});
		fTable.addDisposeListener(event -> {
			stop(true, true);
			fSeparatorIcon.dispose();
			fImageManager.dispose();
			if (fProgressUpdateJob != null) {
				fProgressUpdateJob.stop();
				fProgressUpdateJob= null;
			}
		});
		if (VIRTUAL) {
			fHistoryMatches= EMTPY_TYPE_INFO_ARRAY;
			fSearchMatches= EMTPY_TYPE_INFO_ARRAY;
			fTable.addListener(SWT.SetData, event -> {
				TableItem item= (TableItem)event.item;
				setData(item);
			});
		}

		fDashLineColor= computeDashLineColor();
		fScrollbarWidth= computeScrollBarWidth();
		fTableWidthDelta= fTable.computeTrim(0, 0, 0, 0).width - fScrollbarWidth;
		fSeparatorIcon= JavaPluginImages.DESC_OBJS_TYPE_SEPARATOR.createImage(fTable.getDisplay());
		// Use a new image manager since an extension can provide its own
		// image descriptors. To avoid thread problems with SWT the registry
		// must be created in the UI thread.
		fImageManager= new ImageManager();

		fHistory= OpenTypeHistory.getInstance();
		if (initialFilter != null && initialFilter.length() > 0)
			fTypeInfoFilter= createTypeInfoFilter(initialFilter);
		GC gc= null;
		try {
			gc= new GC(fTable);
			gc.setFont(fTable.getFont());
			fDashLine.initialize(gc);
		} finally {
			gc.dispose();
		}
		// If we do have a type info filter then we are
		// scheduling a search job in startup. So no
		// need to sync the search indices.
		if (fTypeInfoFilter == null) {
			scheduleSyncJob();
		}
	}

	/* package */ void startup() {
		if (fTypeInfoFilter == null) {
			reset();
		} else {
			scheduleSearchJob(FULL);
		}
	}

	public Table getTable() {
		return fTable;
	}

	/* package */ TypeInfoLabelProvider getLabelProvider() {
		return fLabelProvider;
	}

	private int getNumberOfVisibleItems() {
		return fNumberOfVisibleItems;
	}

	public void setFocus() {
		fTable.setFocus();
	}


	public void setQualificationStyle(boolean value) {
		if (fFullyQualifySelection == value)
			return;
		fFullyQualifySelection= value;
		if (fLastSelection != null) {
			for (TableItem item : fLastSelection) {
				Object data= item.getData();
				if (data instanceof TypeNameMatch) {
					item.setText(getQualifiedText((TypeNameMatch)data));
				}
			}
		}
	}

	public TypeNameMatch[] getSelection() {
		TableItem[] items= fTable.getSelection();
		List result= new ArrayList(items.length);
		for (TableItem item : items) {
			Object data= item.getData();
			if (data instanceof TypeNameMatch) {
				result.add(data);
			}
		}
		return (TypeNameMatch[])result.toArray(new TypeNameMatch[result.size()]);
	}

	public void stop() {
		stop(true, false);
	}

	public void stop(boolean stopSyncJob, boolean dispose) {
		if (fSyncJob != null && stopSyncJob) {
			fSyncJob.stop();
			fSyncJob= null;
		}
		if (fSearchJob != null) {
			fSearchJob.stop();
			fSearchJob= null;
		}
	}

	public void forceSearch() {
		stop(false, false);
		if (fTypeInfoFilter == null) {
			reset();
		} else {
			// clear last results
			fLastCompletedFilter= null;
			fLastCompletedResult= null;
			scheduleSearchJob(isSyncJobRunning() ? HISTORY : FULL);
		}
	}

	public void setSearchPattern(String text) {
		stop(false, false);
		if (text.length() == 0 || "*".equals(text)) { //$NON-NLS-1$
			fTypeInfoFilter= null;
			reset();
		} else {
			fTypeInfoFilter= createTypeInfoFilter(text);
			scheduleSearchJob(isSyncJobRunning() ? HISTORY : FULL);
		}
	}

	public void setSearchScope(IJavaSearchScope scope, boolean refresh) {
		fSearchScope= scope;
		if (!refresh)
			return;
		stop(false, false);
		fLastCompletedFilter= null;
		fLastCompletedResult= null;
		if (fTypeInfoFilter == null) {
			reset();
		} else {
			scheduleSearchJob(isSyncJobRunning() ? HISTORY : FULL);
		}
	}

	public void setFullyQualifyDuplicates(boolean value, boolean refresh) {
		fLabelProvider.setFullyQualifyDuplicates(value);
		if (!refresh)
			return;
		stop(false, false);
		if (fTypeInfoFilter == null) {
			reset();
		} else {
			scheduleSearchJob(isSyncJobRunning() ? HISTORY : FULL);
		}
	}

	public void reset() {
		fLastSelection= null;
		fLastLabels= null;
		fExpectedItemCount= 0;
		fDashLineIndex= -1;
		TypeInfoFilter filter= (fTypeInfoFilter != null)
			? fTypeInfoFilter
			: new TypeInfoFilter("*", fSearchScope, fElementKind, fFilterExtension); //$NON-NLS-1$
		if (VIRTUAL) {
			fHistoryMatches= fHistory.getFilteredTypeInfos(filter);
			fExpectedItemCount= fHistoryMatches.length;
			fTable.setItemCount(fHistoryMatches.length);
			// bug under windows.
			if (fHistoryMatches.length == 0) {
				fTable.redraw();
			}
			fTable.clear(0, fHistoryMatches.length - 1);
		} else {
			fNextElement= 0;
			TypeNameMatch[] historyItems= fHistory.getFilteredTypeInfos(filter);
			if (historyItems.length == 0) {
				shortenTable();
				return;
			}
			fExpectedItemCount= historyItems.length;
			int lastIndex= historyItems.length - 1;
			TypeNameMatch last= null;
			TypeNameMatch type= historyItems[0];
			for (int i= 0; i < historyItems.length; i++) {
				TypeNameMatch next= i == lastIndex ? null : historyItems[i + 1];
				addSingleElement(type,
					fLabelProvider.getImageDescriptor(type),
					fLabelProvider.getText(last, type, next));
				last= type;
				type= next;
			}
			shortenTable();
		}
	}

	protected TypeInfoFilter createTypeInfoFilter(String text) {
		if ("**".equals(text)) //$NON-NLS-1$
			text= "*"; //$NON-NLS-1$
		return new TypeInfoFilter(text, fSearchScope, fElementKind, fFilterExtension);
	}

	private void addPopupMenu() {
		Menu menu= new Menu(fTable.getShell(), SWT.POP_UP);
		fTable.setMenu(menu);
		final MenuItem remove= new MenuItem(menu, SWT.NONE);
		remove.setText(JavaUIMessages.TypeInfoViewer_remove_from_history);
		menu.addMenuListener(new MenuAdapter() {
			@Override
			public void menuShown(MenuEvent e) {
				TableItem[] selection= fTable.getSelection();
				remove.setEnabled(canEnable(selection));
			}
		});
		remove.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				deleteHistoryEntry();
			}
		});
	}

	private boolean canEnable(TableItem[] selection) {
		if (selection.length == 0)
			return false;
		for (TableItem item : selection) {
			Object data= item.getData();
			if (!(data instanceof TypeNameMatch))
				return false;
			if (!(fHistory.contains((TypeNameMatch)data)))
				return false;
		}
		return true;
	}

	//---- History management -------------------------------------------------------

	private void deleteHistoryEntry() {
		int index= fTable.getSelectionIndex();
		if (index == -1)
			return;
		TableItem item= fTable.getItem(index);
		Object element= item.getData();
		if (!(element instanceof TypeNameMatch))
			return;
		if (fHistory.remove((TypeNameMatch) element) != null) {
			item.dispose();
			fItems.remove(index);
			int count= fTable.getItemCount();
			if (count > 0) {
				item= fTable.getItem(0);
				if (item.getData() instanceof DashLine) {
					item.dispose();
					fItems.remove(0);
					fDashLineIndex= -1;
					if (count > 1) {
						setTableSelection(0);
					}
				} else {
					if (index >= count) {
						index= count - 1;
					}
					setTableSelection(index);
				}
			} else {
				// send dummy selection
				fTable.notifyListeners(SWT.Selection, new Event());
			}
		}
	}

	//-- Search result updating ----------------------------------------------------

	private void clear(int ticket) {
		syncExec(ticket, () -> {
			fNextElement= 0;
			fDashLineIndex= -1;
			fLastSelection= null;
			fLastLabels= null;
			fExpectedItemCount= 0;
		});
	}

	private void rememberResult(int ticket, final TypeNameMatch[] result) {
		syncExec(ticket, () -> {
			if (fLastCompletedResult == null) {
				fLastCompletedFilter= fTypeInfoFilter;
				fLastCompletedResult= result;
			}
		});
	}

	private void addHistory(int ticket, final List elements, final List imageDescriptors, final List labels) {
		addAll(ticket, elements, imageDescriptors, labels);
	}

	private void addAll(int ticket, final List elements, final List imageDescriptors, final List labels) {
		syncExec(ticket, () -> {
			int size= elements.size();
			for(int i= 0; i < size; i++) {
				addSingleElement(elements.get(i),
					(ImageDescriptor)imageDescriptors.get(i),
					(String)labels.get(i));
			}
		});
	}

	private void addDashLineAndUpdateLastHistoryEntry(int ticket, final TypeNameMatch next) {
		syncExec(ticket, () -> {
			if (fNextElement > 0) {
				TableItem item= fTable.getItem(fNextElement - 1);
				String label= item.getText();
				String newLabel= fLabelProvider.getText(null, (TypeNameMatch)item.getData(), next);
				if (newLabel.length() != label.length())
					item.setText(newLabel);
				if (fLastSelection != null && fLastSelection.length > 0) {
					TableItem last= fLastSelection[fLastSelection.length - 1];
					if (last == item) {
						fLastLabels[fLastLabels.length - 1]= newLabel;
					}
				}
			}
			fDashLineIndex= fNextElement;
			addDashLine();
		});
	}

	private void addDashLine() {
		TableItem item= null;
		if (fItems.size() > fNextElement) {
			item= (TableItem)fItems.get(fNextElement);
		} else {
			item= new TableItem(fTable, SWT.NONE);
			fItems.add(item);
		}
		fillDashLine(item);
		fNextElement++;
	}

	private void addSingleElement(Object element, ImageDescriptor imageDescriptor, String label) {
		TableItem item= null;
		Object old= null;
		if (fItems.size() > fNextElement) {
			item= (TableItem)fItems.get(fNextElement);
			old= item.getData();
			item.setForeground(null);
		} else {
			item= new TableItem(fTable, SWT.NONE);
			fItems.add(item);
		}
		item.setData(element);
		item.setImage(fImageManager.get(imageDescriptor));
		if (fNextElement == 0) {
			if (needsSelectionChange(old, element) || fLastSelection != null) {
				item.setText(label);
				fTable.setSelection(0);
	            fTable.notifyListeners(SWT.Selection, new Event());
			} else {
				fLastSelection= new TableItem[] { item };
				fLastLabels= new String[] { label };
			}
		} else {
			item.setText(label);
		}
		fNextElement++;
	}

	private boolean needsSelectionChange(Object oldElement, Object newElement) {
		int[] selected= fTable.getSelectionIndices();
		if (selected.length != 1)
			return true;
		if (selected[0] != 0)
			return true;
		if (oldElement == null)
			return true;
		return !oldElement.equals(newElement);
	}

	private void scheduleSearchJob(int mode) {
		fSearchJobTicket++;
		if (fLastCompletedFilter != null && fTypeInfoFilter.isSubFilter(fLastCompletedFilter.getText())) {
			fSearchJob= new CachedResultJob(fSearchJobTicket, fLastCompletedResult, this, fTypeInfoFilter,
				fHistory, fNumberOfVisibleItems,
				mode);
		} else {
			fLastCompletedFilter= null;
			fLastCompletedResult= null;
			fSearchJob= new SearchEngineJob(fSearchJobTicket, this, fTypeInfoFilter,
				fHistory, fNumberOfVisibleItems,
				mode, fSearchScope, fElementKind);
		}
		fSearchJob.schedule();
	}

	private void searchJobDone(int ticket) {
		syncExec(ticket, () -> {
			shortenTable();
			checkEmptyList();
			fSearchJob= null;
		});
	}

	private void searchJobCanceled(int ticket, final boolean removePendingItems) {
		syncExec(ticket, () -> {
			if (removePendingItems) {
				shortenTable();
				checkEmptyList();
			}
			fSearchJob= null;
		});
	}

	private synchronized void searchJobFailed(int ticket, CoreException e) {
		searchJobDone(ticket);
		JavaPlugin.log(e);
	}

	//-- virtual table support -------------------------------------------------------

	private void setHistoryResult(int ticket, final TypeNameMatch[] types) {
		syncExec(ticket, () -> {
			fExpectedItemCount= types.length;
			int lastHistoryLength= fHistoryMatches.length;
			fHistoryMatches= types;
			int length= fHistoryMatches.length + fSearchMatches.length;
			int dash= (fHistoryMatches.length > 0 && fSearchMatches.length > 0) ? 1 : 0;
			fTable.setItemCount(length + dash);
			if (length == 0) {
				// bug under windows.
				fTable.redraw();
				return;
			}
			int update= Math.max(lastHistoryLength, fHistoryMatches.length);
			if (update > 0) {
				fTable.clear(0, update + dash - 1);
			}
		});
	}

	private void setSearchResult(int ticket, final TypeNameMatch[] types) {
		syncExec(ticket, () -> {
			fExpectedItemCount+= types.length;
			fSearchMatches= types;
			int length= fHistoryMatches.length + fSearchMatches.length;
			int dash= (fHistoryMatches.length > 0 && fSearchMatches.length > 0) ? 1 : 0;
			fTable.setItemCount(length + dash);
			if (length == 0) {
				// bug under windows.
				fTable.redraw();
				return;
			}
			if (fHistoryMatches.length == 0) {
				fTable.clear(0, length + dash - 1);
			} else {
				fTable.clear(fHistoryMatches.length - 1, length + dash - 1);
			}
		});
	}

	private void setData(TableItem item) {
		int index= fTable.indexOf(item);
		TypeNameMatch type= getTypeInfo(index);
		if (type == DASH_LINE) {
			item.setData(fDashLine);
			fillDashLine(item);
		} else {
			item.setData(type);
			item.setImage(fImageManager.get(fLabelProvider.getImageDescriptor(type)));
			item.setText(fLabelProvider.getText(
				getTypeInfo(index - 1),
				type,
				getTypeInfo(index + 1)));
			item.setForeground(null);
		}
	}

	private TypeNameMatch getTypeInfo(int index) {
		if (index < 0)
			return null;
		if (index < fHistoryMatches.length) {
			return fHistoryMatches[index];
		}
		int dash= (fHistoryMatches.length > 0 && fSearchMatches.length > 0) ? 1 : 0;
		if (index == fHistoryMatches.length && dash == 1) {
			return DASH_LINE;
		}
		index= index - fHistoryMatches.length - dash;
		if (index >= fSearchMatches.length)
			return null;
		return fSearchMatches[index];
	}

	//-- Sync Job updates ------------------------------------------------------------

	private void scheduleSyncJob() {
		fSyncJob= new SyncJob(this);
		fSyncJob.schedule();
	}

	private void syncJobDone() {
		syncExec(() -> {
			fSyncJob= null;
			if (fTypeInfoFilter != null) {
				scheduleSearchJob(FULL);
			}
		});
	}

	private boolean isSyncJobRunning() {
		return fSyncJob != null;
	}

	//-- progress monitor updates -----------------------------------------------------

	private void scheduleProgressUpdateJob() {
		syncExec(() -> {
			if (fProgressCounter == 0) {
				clearProgressMessage();
				fProgressUpdateJob= new ProgressUpdateJob(fDisplay, this);
				fProgressUpdateJob.schedule(300);
			}
			fProgressCounter++;
		});
	}

	private void stopProgressUpdateJob() {
		syncExec(() -> {
			fProgressCounter--;
			if (fProgressCounter == 0 && fProgressUpdateJob != null) {
				fProgressUpdateJob.stop();
				fProgressUpdateJob= null;
				clearProgressMessage();
			}
		});
	}

	private void setProgressMessage(String message) {
		fProgressMessage= message;
	}

	private void clearProgressMessage() {
		fProgressMessage= ""; //$NON-NLS-1$
		fProgressLabel.setText(fProgressMessage);
	}

	private void updateProgressMessage() {
		fProgressLabel.setText(fProgressMessage);
	}

	//-- Helper methods --------------------------------------------------------------

	private void syncExec(final Runnable runnable) {
		if (fDisplay.isDisposed())
			return;
		fDisplay.syncExec(() -> {
			if (fTable.isDisposed())
				return;
			runnable.run();
		});
	}

	private void syncExec(final int ticket, final Runnable runnable) {
		if (fDisplay.isDisposed())
			return;
		fDisplay.syncExec(() -> {
			if (fTable.isDisposed() || ticket != fSearchJobTicket)
				return;
			runnable.run();
		});
	}

	private void fillDashLine(TableItem item) {
		Rectangle bounds= item.getImageBounds(0);
		Rectangle area= fTable.getBounds();
		boolean willHaveScrollBar= fExpectedItemCount + 1 > fNumberOfVisibleItems;
		item.setText(fDashLine.getText(area.width - bounds.x - bounds.width - fTableWidthDelta -
			(willHaveScrollBar ? fScrollbarWidth : 0)));
		item.setImage(fSeparatorIcon);
		item.setForeground(fDashLineColor);
		item.setData(fDashLine);
	}

	private void shortenTable() {
		if (VIRTUAL)
			return;
        if (fNextElement < fItems.size()) {
            fTable.setRedraw(false);
            fTable.remove(fNextElement, fItems.size() - 1);
            fTable.setRedraw(true);
        }
		for (int i= fItems.size() - 1; i >= fNextElement; i--) {
			fItems.remove(i);
		}
	}

	private void checkEmptyList() {
		if (fTable.getItemCount() == 0) {
			fTable.notifyListeners(SWT.Selection, new Event());
		}
	}

	private void setTableSelection(int index) {
		fTable.setSelection(index);
		fTable.notifyListeners(SWT.Selection, new Event());
	}

	private Color computeDashLineColor() {
		Color fg= fTable.getForeground();
		int fGray= (int)(0.3*fg.getRed() + 0.59*fg.getGreen() + 0.11*fg.getBlue());
		Color bg= fTable.getBackground();
		int bGray= (int)(0.3*bg.getRed() + 0.59*bg.getGreen() + 0.11*bg.getBlue());
		int gray= (int)((fGray + bGray) * 0.66);
		return new Color(fDisplay, gray, gray, gray);
	}

	private int computeScrollBarWidth() {
		Composite t= new Composite(fTable.getShell(), SWT.V_SCROLL);
		int result= t.computeTrim(0, 0, 0, 0).width;
		t.dispose();
		return result;
	}

	private String getQualifiedText(TypeNameMatch type) {
		return fFullyQualifySelection
			? fLabelProvider.getFullyQualifiedText(type)
			: fLabelProvider.getQualifiedText(type);
	}
}
