/*******************************************************************************
 * Copyright (c) 2000, 2015 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
 * 	Sebastian Davids <sdavids@gmx.de> - Fix for bug 19346 - Dialog font
 * 		should be activated and used by other components.
 *******************************************************************************/
package org.eclipse.ui.dialogs;

import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.text.StringMatcher;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
import org.eclipse.ui.model.WorkbenchLabelProvider;
import org.osgi.framework.FrameworkUtil;

/**
 * Shows a list of resources to the user with a text entry field for a string
 * pattern used to filter the list of resources.
 *
 * @since 2.1
 */
public class ResourceListSelectionDialog extends SelectionDialog {

	private static final String DIALOG_SETTINGS_SECTION = "ResourceListSelectionDialogSettings"; //$NON-NLS-1$

	Text pattern;

	Table resourceNames;

	Table folderNames;

	String patternString;

	IContainer container;

	int typeMask;

	private static Collator collator = Collator.getInstance();

	boolean gatherResourcesDynamically = true;

	StringMatcher stringMatcher;

	UpdateFilterThread updateFilterThread;

	UpdateGatherThread updateGatherThread;

	ResourceDescriptor[] descriptors;

	int descriptorsSize;

	WorkbenchLabelProvider labelProvider = new WorkbenchLabelProvider();

	boolean okEnabled = false;

	private boolean showDerived = false;

	private Button showDerivedButton;

	private boolean allowUserToToggleDerived;

	static class ResourceDescriptor implements Comparable {
		String label;

		ArrayList resources = new ArrayList();

		boolean resourcesSorted = true;

		@Override
		public int compareTo(Object o) {
			return collator.compare(label, ((ResourceDescriptor) o).label);
		}
	}

	class UpdateFilterThread extends Thread {
		boolean stop = false;

		int firstMatch = 0;

		int lastMatch = descriptorsSize - 1;

		@Override
		public void run() {
			Display display = resourceNames.getDisplay();
			final int itemIndex[] = { 0 };
			final int itemCount[] = { 0 };
			// Keep track of if the widget got disposed
			// so that we can abort if required
			final boolean[] disposed = { false };
			display.syncExec(() -> {
				// Be sure the widget still exists
				if (resourceNames.isDisposed()) {
					disposed[0] = true;
					return;
				}
				itemCount[0] = resourceNames.getItemCount();
			});

			if (disposed[0]) {
				return;
			}

			int last;
			if ((patternString.indexOf('?') == -1) && (patternString.endsWith("*")) && //$NON-NLS-1$
					(patternString.indexOf('*') == patternString.length() - 1)) {
				// Use a binary search to get first and last match when the pattern
				// string ends with "*" and has no other embedded special characters.
				// For this case, we can be smarter about getting the first and last
				// match since the items are in sorted order.
				firstMatch = getFirstMatch();
				if (firstMatch == -1) {
					firstMatch = 0;
					lastMatch = -1;
				} else {
					lastMatch = getLastMatch();
				}
				last = lastMatch;
				for (int i = firstMatch; i <= lastMatch; i++) {
					if (i % 50 == 0) {
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
							// ignore
						}
					}
					if (stop || resourceNames.isDisposed()) {
						disposed[0] = true;
						return;
					}
					final int index = i;
					display.syncExec(() -> {
						if (stop || resourceNames.isDisposed()) {
							return;
						}
						updateItem(index, itemIndex[0], itemCount[0]);
						itemIndex[0]++;
					});
				}
			} else {
				last = lastMatch;
				boolean setFirstMatch = true;
				for (int i = firstMatch; i <= lastMatch; i++) {
					if (i % 50 == 0) {
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
							// ignore
						}
					}
					if (stop || resourceNames.isDisposed()) {
						disposed[0] = true;
						return;
					}
					final int index = i;
					if (match(descriptors[index].label)) {
						if (setFirstMatch) {
							setFirstMatch = false;
							firstMatch = index;
						}
						last = index;
						display.syncExec(() -> {
							if (stop || resourceNames.isDisposed()) {
								return;
							}
							updateItem(index, itemIndex[0], itemCount[0]);
							itemIndex[0]++;
						});
					}
				}
			}

			if (disposed[0]) {
				return;
			}

			lastMatch = last;
			display.syncExec(() -> {
				if (resourceNames.isDisposed()) {
					return;
				}
				itemCount[0] = resourceNames.getItemCount();
				if (itemIndex[0] < itemCount[0]) {
					resourceNames.setRedraw(false);
					resourceNames.remove(itemIndex[0], itemCount[0] - 1);
					resourceNames.setRedraw(true);
				}
				// If no resources, remove remaining folder entries
				if (resourceNames.getItemCount() == 0) {
					folderNames.removeAll();
					updateOKState(false);
				}
			});
		}
	}

	class UpdateGatherThread extends Thread {
		boolean stop = false;

		int lastMatch = -1;

		int firstMatch = 0;

		boolean refilter = false;

		@Override
		public void run() {
			Display display = resourceNames.getDisplay();
			final int itemIndex[] = { 0 };
			final int itemCount[] = { 0 };
			// Keep track of if the widget got disposed
			// so that we can abort if required
			final boolean[] disposed = { false };
			display.syncExec(() -> {
				// Be sure the widget still exists
				if (resourceNames.isDisposed()) {
					disposed[0] = true;
					return;
				}
				itemCount[0] = resourceNames.getItemCount();
			});

			if (disposed[0]) {
				return;
			}

			if (!refilter) {
				for (int i = 0; i <= lastMatch; i++) {
					if (i % 50 == 0) {
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
							// ignore
						}
					}
					if (stop || resourceNames.isDisposed()) {
						disposed[0] = true;
						return;
					}
					final int index = i;
					display.syncExec(() -> {
						if (stop || resourceNames.isDisposed()) {
							return;
						}
						updateItem(index, itemIndex[0], itemCount[0]);
						itemIndex[0]++;
					});
				}
			} else {
				// we're filtering the previous list
				for (int i = firstMatch; i <= lastMatch; i++) {
					if (i % 50 == 0) {
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
							// ignore
						}
					}
					if (stop || resourceNames.isDisposed()) {
						disposed[0] = true;
						return;
					}
					final int index = i;
					if (match(descriptors[index].label)) {
						display.syncExec(() -> {
							if (stop || resourceNames.isDisposed()) {
								return;
							}
							updateItem(index, itemIndex[0], itemCount[0]);
							itemIndex[0]++;
						});
					}
				}
			}

			if (disposed[0]) {
				return;
			}

			display.syncExec(() -> {
				if (resourceNames.isDisposed()) {
					return;
				}
				itemCount[0] = resourceNames.getItemCount();
				if (itemIndex[0] < itemCount[0]) {
					resourceNames.setRedraw(false);
					resourceNames.remove(itemIndex[0], itemCount[0] - 1);
					resourceNames.setRedraw(true);
				}
				// If no resources, remove remaining folder entries
				if (resourceNames.getItemCount() == 0) {
					folderNames.removeAll();
					updateOKState(false);
				}
			});
		}
	}

	/**
	 * Creates a new instance of the class.
	 *
	 * @param parentShell shell to parent the dialog on
	 * @param resources   resources to display in the dialog
	 */
	public ResourceListSelectionDialog(Shell parentShell, IResource[] resources) {
		super(parentShell);
		gatherResourcesDynamically = false;
		initDescriptors(resources);
	}

	/**
	 * Creates a new instance of the class. When this constructor is used to create
	 * the dialog, resources will be gathered dynamically as the pattern string is
	 * specified. Only resources of the given types that match the pattern string
	 * will be listed. To further filter the matching resources,
	 *
	 * @see #select(IResource)
	 *
	 * @param parentShell shell to parent the dialog on
	 * @param container   container to get resources from
	 * @param typeMask    mask containing IResource types to be considered
	 */
	public ResourceListSelectionDialog(Shell parentShell, IContainer container, int typeMask) {
		super(parentShell);
		this.container = container;
		this.typeMask = typeMask;
	}

	/**
	 * Adjust the pattern string for matching.
	 */
	protected String adjustPattern() {
		String text = pattern.getText().trim();
		if (text.endsWith("<")) { //$NON-NLS-1$
			// the < character indicates an exact match search
			return text.substring(0, text.length() - 1);
		}
		if (!text.isEmpty() && !text.endsWith("*")) { //$NON-NLS-1$
			return text + "*"; //$NON-NLS-1$
		}
		return text;
	}

	/**
	 * @see org.eclipse.jface.dialogs.Dialog#cancelPressed()
	 */
	@Override
	protected void cancelPressed() {
		setResult(null);
		super.cancelPressed();
	}

	/**
	 * @see org.eclipse.jface.window.Window#close()
	 */
	@Override
	public boolean close() {
		boolean result = super.close();
		labelProvider.dispose();
		return result;
	}

	/**
	 * @see org.eclipse.jface.window.Window#create()
	 */
	@Override
	public void create() {
		super.create();
		pattern.setFocus();
		getButton(IDialogConstants.OK_ID).setEnabled(okEnabled);
	}

	/**
	 * Creates the contents of this dialog, initializes the listener and the update
	 * thread.
	 *
	 * @param parent parent to create the dialog widgets in
	 */
	@Override
	protected Control createDialogArea(Composite parent) {

		Composite dialogArea = (Composite) super.createDialogArea(parent);
		Label l = new Label(dialogArea, SWT.NONE);
		l.setText(IDEWorkbenchMessages.ResourceSelectionDialog_label);
		GridData data = new GridData(GridData.FILL_HORIZONTAL);
		l.setLayoutData(data);

		pattern = new Text(dialogArea, SWT.SINGLE | SWT.BORDER);
		pattern.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
		l = new Label(dialogArea, SWT.NONE);
		l.setText(IDEWorkbenchMessages.ResourceSelectionDialog_matching);
		data = new GridData(GridData.FILL_HORIZONTAL);
		l.setLayoutData(data);
		resourceNames = new Table(dialogArea, SWT.SINGLE | SWT.BORDER | SWT.V_SCROLL);
		data = new GridData(GridData.FILL_BOTH);
		data.heightHint = 12 * resourceNames.getItemHeight();
		resourceNames.setLayoutData(data);

		l = new Label(dialogArea, SWT.NONE);
		l.setText(IDEWorkbenchMessages.ResourceSelectionDialog_folders);
		data = new GridData(GridData.FILL_HORIZONTAL);
		l.setLayoutData(data);

		folderNames = new Table(dialogArea, SWT.SINGLE | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
		data = new GridData(GridData.FILL_BOTH);
		data.widthHint = 300;
		data.heightHint = 4 * folderNames.getItemHeight();
		folderNames.setLayoutData(data);

		if (gatherResourcesDynamically) {
			updateGatherThread = new UpdateGatherThread();
		} else {
			updateFilterThread = new UpdateFilterThread();
		}

		pattern.addKeyListener(new KeyAdapter() {
			@Override
			public void keyReleased(KeyEvent e) {
				if (e.keyCode == SWT.ARROW_DOWN) {
					resourceNames.setFocus();
				}
			}
		});

		pattern.addModifyListener(e -> refresh(false));

		resourceNames.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				updateFolders((ResourceDescriptor) e.item.getData());
			}

			@Override
			public void widgetDefaultSelected(SelectionEvent e) {
				okPressed();
			}
		});

		folderNames.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetDefaultSelected(SelectionEvent e) {
				okPressed();
			}
		});

		if (getAllowUserToToggleDerived()) {
			showDerivedButton = new Button(dialogArea, SWT.CHECK);
			showDerivedButton.setText(IDEWorkbenchMessages.ResourceSelectionDialog_showDerived);
			showDerivedButton.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					setShowDerived(showDerivedButton.getSelection());
					refresh(true);
				}
			});
			showDerivedButton.setSelection(getShowDerived());
		}

		applyDialogFont(dialogArea);
		return dialogArea;
	}

	/**
	 * Returns whether to include a "Show derived resources" checkbox in the dialog.
	 * The default is <code>false</code>.
	 *
	 * @return <code>true</code> to include the checkbox, <code>false</code> to omit
	 * @since 3.1
	 */
	public boolean getAllowUserToToggleDerived() {
		return allowUserToToggleDerived;
	}

	/**
	 * Sets whether to include a "Show derived resources" checkbox in the dialog.
	 *
	 * @param allow <code>true</code> to include the checkbox, <code>false</code> to
	 *              omit
	 * @since 3.1
	 */
	public void setAllowUserToToggleDerived(boolean allow) {
		allowUserToToggleDerived = allow;
	}

	/**
	 */
	private void filterResources(boolean force) {
		String oldPattern = force ? null : patternString;
		patternString = adjustPattern();
		if (!force && patternString.equals(oldPattern)) {
			return;
		}

		updateFilterThread.stop = true;
		stringMatcher = new StringMatcher(patternString, true, false);
		UpdateFilterThread oldThread = updateFilterThread;
		updateFilterThread = new UpdateFilterThread();
		if (patternString.isEmpty()) {
			updateFilterThread.firstMatch = 0;
			updateFilterThread.lastMatch = -1;
			updateFilterThread.start();
			return;
		}

		if (oldPattern != null && (oldPattern.length() != 0) && oldPattern.endsWith("*") //$NON-NLS-1$
				&& patternString.endsWith("*")) { //$NON-NLS-1$
			int matchLength = oldPattern.length() - 1;
			if (patternString.regionMatches(0, oldPattern, 0, matchLength)) {
				// filter the previous list of items, this is done when the
				// new pattern is a derivative of the old pattern
				updateFilterThread.firstMatch = oldThread.firstMatch;
				updateFilterThread.lastMatch = oldThread.lastMatch;
				updateFilterThread.start();
				return;
			}
		}

		// filter the entire list
		updateFilterThread.firstMatch = 0;
		updateFilterThread.lastMatch = descriptorsSize - 1;
		updateFilterThread.start();
	}

	/**
	 * Use a binary search to get the first match for the patternString. This method
	 * assumes the patternString does not contain any '?' characters and that it
	 * contains only one '*' character at the end of the string.
	 */
	private int getFirstMatch() {
		int high = descriptorsSize;
		int low = -1;
		boolean match = false;
		ResourceDescriptor desc = new ResourceDescriptor();
		desc.label = patternString.substring(0, patternString.length() - 1);
		while (high - low > 1) {
			int index = (high + low) / 2;
			String label = descriptors[index].label;
			if (match(label)) {
				high = index;
				match = true;
			} else {
				int compare = descriptors[index].compareTo(desc);
				if (compare < 0) {
					low = index;
				} else {
					high = index;
				}
			}
		}
		if (match) {
			return high;
		}
		return -1;
	}

	/**
	 */
	private void gatherResources(boolean force) {
		String oldPattern = force ? null : patternString;
		patternString = adjustPattern();
		if (!force && patternString.equals(oldPattern)) {
			return;
		}

		updateGatherThread.stop = true;
		updateGatherThread = new UpdateGatherThread();

		if (patternString.isEmpty()) {
			updateGatherThread.start();
			return;
		}
		stringMatcher = new StringMatcher(patternString, true, false);

		if (oldPattern != null && (oldPattern.length() != 0) && oldPattern.endsWith("*") //$NON-NLS-1$
				&& patternString.endsWith("*")) { //$NON-NLS-1$
			// see if the new pattern is a derivative of the old pattern
			int matchLength = oldPattern.length() - 1;
			if (patternString.regionMatches(0, oldPattern, 0, matchLength)) {
				updateGatherThread.refilter = true;
				updateGatherThread.firstMatch = 0;
				updateGatherThread.lastMatch = descriptorsSize - 1;
				updateGatherThread.start();
				return;
			}
		}

		final ArrayList<IResource> resources = new ArrayList<>();
		BusyIndicator.showWhile(getShell().getDisplay(), () -> {
			getMatchingResources(resources);
			IResource resourcesArray[] = new IResource[resources.size()];
			resources.toArray(resourcesArray);
			initDescriptors(resourcesArray);
		});

		updateGatherThread.firstMatch = 0;
		updateGatherThread.lastMatch = descriptorsSize - 1;
		updateGatherThread.start();
	}

	/**
	 * Return an image for a resource descriptor.
	 *
	 * @param desc resource descriptor to return image for
	 * @return an image for a resource descriptor.
	 */
	private Image getImage(ResourceDescriptor desc) {
		IResource r = (IResource) desc.resources.get(0);
		return labelProvider.getImage(r);
	}

	/**
	 * Use a binary search to get the last match for the patternString. This method
	 * assumes the patternString does not contain any '?' characters and that it
	 * contains only one '*' character at the end of the string.
	 */
	private int getLastMatch() {
		int high = descriptorsSize;
		int low = -1;
		boolean match = false;
		ResourceDescriptor desc = new ResourceDescriptor();
		desc.label = patternString.substring(0, patternString.length() - 1);
		while (high - low > 1) {
			int index = (high + low) / 2;
			String label = descriptors[index].label;
			if (match(label)) {
				low = index;
				match = true;
			} else {
				int compare = descriptors[index].compareTo(desc);
				if (compare < 0) {
					low = index;
				} else {
					high = index;
				}
			}
		}
		if (match) {
			return low;
		}
		return -1;
	}

	/**
	 * Gather the resources of the specified type that match the current pattern
	 * string. Gather the resources using the proxy visitor since this is quicker
	 * than getting the entire resource.
	 *
	 * @param resources resources that match
	 */
	private void getMatchingResources(final ArrayList<IResource> resources) {
		try {
			container.accept(proxy -> {
				// optionally exclude derived resources (bugs 38085 and 81333)
				if (!getShowDerived() && proxy.isDerived()) {
					return false;
				}
				int type = proxy.getType();
				if ((typeMask & type) != 0) {
					if (match(proxy.getName())) {
						IResource res = proxy.requestResource();
						if (select(res)) {
							resources.add(res);
							return true;
						}
						return false;
					}
				}
				if (type == IResource.FILE) {
					return false;
				}
				return true;
			}, IResource.NONE);
		} catch (CoreException e) {
			// ignore
		}
	}

	private Image getParentImage(IResource resource) {
		IResource parent = resource.getParent();
		return labelProvider.getImage(parent);
	}

	private String getParentLabel(IResource resource) {
		IResource parent = resource.getParent();
		String text;
		if (parent.getType() == IResource.ROOT) {
			// Get readable name for workspace root ("Workspace"), without duplicating
			// language-specific string here.
			text = labelProvider.getText(parent);
		} else {
			text = parent.getFullPath().makeRelative().toString();
		}
		if (text == null) {
			return ""; //$NON-NLS-1$
		}
		return text;
	}

	/**
	 * Returns whether derived resources should be shown in the list. The default is
	 * <code>false</code>.
	 *
	 * @return <code>true</code> to show derived resources, <code>false</code> to
	 *         hide them
	 * @since 3.1
	 */
	protected boolean getShowDerived() {
		return showDerived;
	}

	/**
	 * Sets whether derived resources should be shown in the list.
	 *
	 * @param show <code>true</code> to show derived resources, <code>false</code>
	 *             to hide them
	 * @since 3.1
	 */
	protected void setShowDerived(boolean show) {
		showDerived = show;
	}

	/**
	 * Creates a ResourceDescriptor for each IResource, sorts them and removes the
	 * duplicated ones.
	 *
	 * @param resources resources to create resource descriptors for
	 */
	private void initDescriptors(final IResource resources[]) {
		BusyIndicator.showWhile(null, () -> {
			descriptors = new ResourceDescriptor[resources.length];
			for (int i1 = 0; i1 < resources.length; i1++) {
				IResource r = resources[i1];
				ResourceDescriptor d = new ResourceDescriptor();
				// TDB: Should use the label provider and compare performance.
				d.label = r.getName();
				d.resources.add(r);
				descriptors[i1] = d;
			}
			Arrays.sort(descriptors);
			descriptorsSize = descriptors.length;

			// Merge the resource descriptor with the same label and type.
			int index = 0;
			if (descriptorsSize < 2) {
				return;
			}
			ResourceDescriptor current = descriptors[index];
			IResource currentResource = (IResource) current.resources.get(0);
			for (int i2 = 1; i2 < descriptorsSize; i2++) {
				ResourceDescriptor next = descriptors[i2];
				IResource nextResource = (IResource) next.resources.get(0);
				if (nextResource.getType() == currentResource.getType() && next.label.equals(current.label)) {
					current.resources.add(nextResource);
					// If we are merging resources with the same name, into a single descriptor,
					// then we must mark the descriptor unsorted so that we will sort the folder
					// names.
					// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=76496
					current.resourcesSorted = false;
				} else {
					if (current.resources.size() > 1) {
						current.resourcesSorted = false;
					}
					descriptors[index + 1] = descriptors[i2];
					index++;
					current = descriptors[index];
					currentResource = (IResource) current.resources.get(0);
				}
			}
			descriptorsSize = index + 1;
		});
	}

	/**
	 * Returns true if the label matches the chosen pattern.
	 *
	 * @param label label to match with the current pattern
	 * @return true if the label matches the chosen pattern. false otherwise.
	 */
	private boolean match(String label) {
		if ((patternString == null) || (patternString.isEmpty()) || (patternString.equals("*"))) { //$NON-NLS-1$
			return true;
		}
		return stringMatcher.match(label);
	}

	/**
	 * The user has selected a resource and the dialog is closing. Set the selected
	 * resource as the dialog result.
	 */
	@Override
	protected void okPressed() {
		TableItem items[] = folderNames.getSelection();
		if (items.length == 1) {
			ArrayList result = new ArrayList();
			result.add(items[0].getData());
			setResult(result);
		}
		super.okPressed();
	}

	/**
	 * Use this method to further filter resources. As resources are gathered, if a
	 * resource matches the current pattern string, this method will be called. If
	 * this method answers false, the resource will not be included in the list of
	 * matches and the resource's children will NOT be considered for matching.
	 */
	protected boolean select(IResource resource) {
		return true;
	}

	/**
	 * Refreshes the filtered list of resources. Called when the text in the pattern
	 * text entry has changed.
	 *
	 * @param force if <code>true</code> a refresh is forced, if <code>false</code>
	 *              a refresh only occurs if the pattern has changed
	 *
	 * @since 3.1
	 */
	protected void refresh(boolean force) {
		if (gatherResourcesDynamically) {
			gatherResources(force);
		} else {
			filterResources(force);
		}
	}

	/**
	 * A new resource has been selected. Change the contents of the folder names
	 * list.
	 *
	 * @param desc resource descriptor of the selected resource
	 */
	private void updateFolders(final ResourceDescriptor desc) {
		BusyIndicator.showWhile(getShell().getDisplay(), () -> {
			if (!desc.resourcesSorted) {
				// sort the folder names
				desc.resources.sort((o1, o2) -> {
					String s1 = getParentLabel((IResource) o1);
					String s2 = getParentLabel((IResource) o2);
					return collator.compare(s1, s2);
				});
				desc.resourcesSorted = true;
			}
			folderNames.removeAll();
			for (Object element : desc.resources) {
				TableItem newItem = new TableItem(folderNames, SWT.NONE);
				IResource r = (IResource) element;
				newItem.setText(getParentLabel(r));
				newItem.setImage(getParentImage(r));
				newItem.setData(r);
			}
			folderNames.setSelection(0);
		});
	}

	/**
	 * Update the specified item with the new info from the resource descriptor.
	 * Create a new table item if there is no item.
	 *
	 * @param index     index of the resource descriptor
	 * @param itemPos   position of the existing item to update
	 * @param itemCount number of items in the resources table widget
	 */
	private void updateItem(int index, int itemPos, int itemCount) {
		ResourceDescriptor desc = descriptors[index];
		TableItem item;
		if (itemPos < itemCount) {
			item = resourceNames.getItem(itemPos);
			if (item.getData() != desc) {
				item.setText(desc.label);
				item.setData(desc);
				item.setImage(getImage(desc));
				if (itemPos == 0) {
					resourceNames.setSelection(0);
					updateFolders(desc);
				}
			}
		} else {
			item = new TableItem(resourceNames, SWT.NONE);
			item.setText(desc.label);
			item.setData(desc);
			item.setImage(getImage(desc));
			if (itemPos == 0) {
				resourceNames.setSelection(0);
				updateFolders(desc);
			}
		}
		updateOKState(true);
	}

	/**
	 * Update the enabled state of the OK button. To be called when the resource
	 * list is updated.
	 *
	 * @param state the new enabled state of the button
	 */
	protected void updateOKState(boolean state) {
		Button okButton = getButton(IDialogConstants.OK_ID);
		if (okButton != null && !okButton.isDisposed() && state != okEnabled) {
			okButton.setEnabled(state);
			okEnabled = state;
		}
	}

	@Override
	protected IDialogSettings getDialogBoundsSettings() {
		IDialogSettings settings = PlatformUI
				.getDialogSettingsProvider(FrameworkUtil.getBundle(ResourceListSelectionDialog.class))
				.getDialogSettings();
		IDialogSettings section = settings.getSection(DIALOG_SETTINGS_SECTION);
		if (section == null) {
			section = settings.addNewSection(DIALOG_SETTINGS_SECTION);
		}
		return section;
	}
}
