/*******************************************************************************
 * Copyright (c) 2000, 2018 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
 *     Robert Roth (robert.roth.off@gmail.com) - Bug 477471
 *******************************************************************************/
package org.eclipse.search.ui.text;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.LinkedBlockingDeque;

import org.osgi.framework.FrameworkUtil;

import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;

import org.eclipse.core.resources.IFile;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.util.OpenStrategy;
import org.eclipse.jface.viewers.DecoratingLabelProvider;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;

import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;

import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IWorkbenchCommandConstants;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.OpenAndLinkWithEditorHelper;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.part.IPageSite;
import org.eclipse.ui.part.Page;
import org.eclipse.ui.part.PageBook;
import org.eclipse.ui.progress.UIJob;

import org.eclipse.ui.texteditor.IUpdate;

import org.eclipse.search.internal.ui.CopyToClipboardAction;
import org.eclipse.search.internal.ui.SearchPluginImages;
import org.eclipse.search.internal.ui.SelectAllAction;
import org.eclipse.search.internal.ui.text.EditorOpener;
import org.eclipse.search.ui.IContextMenuConstants;
import org.eclipse.search.ui.IQueryListener;
import org.eclipse.search.ui.ISearchQuery;
import org.eclipse.search.ui.ISearchResult;
import org.eclipse.search.ui.ISearchResultListener;
import org.eclipse.search.ui.ISearchResultPage;
import org.eclipse.search.ui.ISearchResultViewPart;
import org.eclipse.search.ui.NewSearchUI;
import org.eclipse.search.ui.SearchResultEvent;

import org.eclipse.search2.internal.ui.InternalSearchUI;
import org.eclipse.search2.internal.ui.MatchFilterAction;
import org.eclipse.search2.internal.ui.MatchFilterSelectionAction;
import org.eclipse.search2.internal.ui.SearchMessages;
import org.eclipse.search2.internal.ui.SearchView;
import org.eclipse.search2.internal.ui.basic.views.CollapseAllAction;
import org.eclipse.search2.internal.ui.basic.views.ExpandAllAction;
import org.eclipse.search2.internal.ui.basic.views.INavigate;
import org.eclipse.search2.internal.ui.basic.views.RemoveAllMatchesAction;
import org.eclipse.search2.internal.ui.basic.views.RemoveMatchAction;
import org.eclipse.search2.internal.ui.basic.views.RemoveSelectedMatchesAction;
import org.eclipse.search2.internal.ui.basic.views.SetLayoutAction;
import org.eclipse.search2.internal.ui.basic.views.ShowNextResultAction;
import org.eclipse.search2.internal.ui.basic.views.ShowPreviousResultAction;
import org.eclipse.search2.internal.ui.basic.views.TableViewerNavigator;
import org.eclipse.search2.internal.ui.basic.views.TreeViewerNavigator;
import org.eclipse.search2.internal.ui.text.AnnotationManagers;
import org.eclipse.search2.internal.ui.text.PositionTracker;

/**
 * An abstract base implementation for classes showing
 * <code>AbstractTextSearchResult</code> instances. This class assumes that
 * the input element set via {@link AbstractTextSearchViewPage#setInput(ISearchResult,Object)}
 * is a subclass of {@link AbstractTextSearchResult}.
 * This result page supports a tree and/or a table presentation of search
 * results. Subclasses can determine which presentations they want to support at
 * construction time by passing the appropriate flags.
 * Subclasses must customize the viewers for each presentation with a label
 * provider and a content provider. <br>
 * Changes in the search result are handled by updating the viewer in the
 * <code>elementsChanged()</code> and <code>clear()</code> methods.
 *
 * @since 3.0
 */
public abstract class AbstractTextSearchViewPage extends Page implements ISearchResultPage {
	private class UpdateUIJob extends UIJob {

		public UpdateUIJob() {
			super(SearchMessages.AbstractTextSearchViewPage_update_job_name);
			setSystem(true);
		}

		@Override
		public IStatus runInUIThread(IProgressMonitor monitor) {
			Control control= getControl();
			if (control == null || control.isDisposed()) {
				// disposed the control while the UI was posted.
				return Status.OK_STATUS;
			}
			runBatchedClear();
			runBatchedUpdates();
			if (hasMoreUpdates() || isQueryRunning()) {
				schedule(500);
			} else {
				fIsUIUpdateScheduled= false;
				turnOnDecoration();
				updateBusyLabel();
				if (fScheduleEnsureSelection) {
					fScheduleEnsureSelection= false;
					AbstractTextSearchResult result = getInput();
					if (result != null && fViewer.getSelection().isEmpty()) {
						navigateNext(true);
					}
				}
			}
			fViewPart.updateLabel();
			return Status.OK_STATUS;
		}

		/*
		 * Undocumented for testing only. Used to find UpdateUIJobs.
		 */
		@Override
		public boolean belongsTo(Object family) {
			return family == AbstractTextSearchViewPage.this;
		}

	}

	private class SelectionProviderAdapter implements ISelectionProvider, ISelectionChangedListener {
		private ArrayList<ISelectionChangedListener> fListeners= new ArrayList<>(5);

		@Override
		public void addSelectionChangedListener(ISelectionChangedListener listener) {
			fListeners.add(listener);
		}

		@Override
		public ISelection getSelection() {
			return fViewer.getSelection();
		}

		@Override
		public void removeSelectionChangedListener(ISelectionChangedListener listener) {
			fListeners.remove(listener);
		}

		@Override
		public void setSelection(ISelection selection) {
			fViewer.setSelection(selection);
		}

		@Override
		public void selectionChanged(SelectionChangedEvent event) {
			// forward to my listeners
			SelectionChangedEvent wrappedEvent= new SelectionChangedEvent(this, event.getSelection());
			for (ISelectionChangedListener listener : fListeners) {
				listener.selectionChanged(wrappedEvent);
			}
		}

	}

	private volatile boolean fIsUIUpdateScheduled= false;
	private volatile boolean fScheduleEnsureSelection= false;
	private static final String KEY_LAYOUT = "org.eclipse.search.resultpage.layout"; //$NON-NLS-1$

	/**
	 * An empty array.
	 */
	protected static final Match[] EMPTY_MATCH_ARRAY= new Match[0];

	private StructuredViewer fViewer;
	private Composite fViewerContainer;
	private Control fBusyLabel;
	private PageBook fPagebook;
	private boolean fIsBusyShown;
	private ISearchResultViewPart fViewPart;
	private final LinkedBlockingDeque<Object> fBatchedUpdates = new LinkedBlockingDeque<>();
	private volatile boolean fBatchedClearAll;

	private ISearchResultListener fListener;
	private IQueryListener fQueryListener;
	private MenuManager fMenu;
	private AbstractTextSearchResult fInput;
	// Actions
	private CopyToClipboardAction fCopyToClipboardAction;
	private Action fRemoveSelectedMatches;
	private Action fRemoveCurrentMatch;
	private Action fRemoveAllResultsAction;
	private Action fShowNextAction;
	private Action fShowPreviousAction;

	private ExpandAllAction fExpandAllAction;
	private CollapseAllAction fCollapseAllAction;

	private SetLayoutAction fFlatAction;
	private SetLayoutAction fHierarchicalAction;
	private int fCurrentLayout;
	private int fCurrentMatchIndex = 0;
	private String fId;
	private final int fSupportedLayouts;
	private SelectionProviderAdapter fViewerAdapter;
	private SelectAllAction fSelectAllAction;

	private IAction[] fFilterActions;
	private Integer fElementLimit;

	/**
	 * The editor opener.
	 * @since 3.6
	 */
	private EditorOpener fEditorOpener= new EditorOpener();

	/**
	 * Flag (<code>value 1</code>) denoting flat list layout.
	 */
	public static final int FLAG_LAYOUT_FLAT = 1;
	/**
	 * Flag (<code>value 2</code>) denoting tree layout.
	 */
	public static final int FLAG_LAYOUT_TREE = 2;


	/**
	 * This constructor must be passed a combination of layout flags combined
	 * with bitwise or. At least one flag must be passed in (i.e. 0 is not a
	 * permitted value).
	 *
	 * @param supportedLayouts
	 *            flags determining which layout options this page supports.
	 *            Must not be 0
	 * @see #FLAG_LAYOUT_FLAT
	 * @see #FLAG_LAYOUT_TREE
	 */
	protected AbstractTextSearchViewPage(int supportedLayouts) {
		fSupportedLayouts = supportedLayouts;
		initLayout();
		fRemoveAllResultsAction = new RemoveAllMatchesAction(this);
		fRemoveSelectedMatches = new RemoveSelectedMatchesAction(this);
		fRemoveCurrentMatch = new RemoveMatchAction(this);
		fShowNextAction = new ShowNextResultAction(this);
		fShowPreviousAction = new ShowPreviousResultAction(this);
		fCopyToClipboardAction = new CopyToClipboardAction();
		if ((supportedLayouts & FLAG_LAYOUT_TREE) != 0) {
			fExpandAllAction= new ExpandAllAction();
			fCollapseAllAction= new CollapseAllAction();
		}

		fSelectAllAction= new SelectAllAction();
		createLayoutActions();

		fListener = this::handleSearchResultChanged;
		fFilterActions= null;
		fElementLimit= null;
	}

	private void initLayout() {
		if (supportsTreeLayout())
			fCurrentLayout = FLAG_LAYOUT_TREE;
		else
			fCurrentLayout = FLAG_LAYOUT_FLAT;
	}

	/**
	 * Constructs this page with the default layout flags.
	 *
	 * @see AbstractTextSearchViewPage#AbstractTextSearchViewPage(int)
	 */
	protected AbstractTextSearchViewPage() {
		this(FLAG_LAYOUT_FLAT | FLAG_LAYOUT_TREE);
	}


	private void createLayoutActions() {
		if (countBits(fSupportedLayouts) > 1) {
			fFlatAction = new SetLayoutAction(this, SearchMessages.AbstractTextSearchViewPage_flat_layout_label, SearchMessages.AbstractTextSearchViewPage_flat_layout_tooltip, FLAG_LAYOUT_FLAT);
			fHierarchicalAction = new SetLayoutAction(this, SearchMessages.AbstractTextSearchViewPage_hierarchical_layout_label, SearchMessages.AbstractTextSearchViewPage_hierarchical_layout_tooltip, FLAG_LAYOUT_TREE);
			SearchPluginImages.setImageDescriptors(fFlatAction, SearchPluginImages.T_LCL, SearchPluginImages.IMG_LCL_SEARCH_FLAT_LAYOUT);
			SearchPluginImages.setImageDescriptors(fHierarchicalAction, SearchPluginImages.T_LCL, SearchPluginImages.IMG_LCL_SEARCH_HIERARCHICAL_LAYOUT);
		}
	}

	private int countBits(int layoutFlags) {
		int bitCount = 0;
		for (int i = 0; i < 32; i++) {
			if (layoutFlags % 2 == 1)
				bitCount++;
			layoutFlags >>= 1;
		}
		return bitCount;
	}

	private boolean supportsTreeLayout() {
		return isLayoutSupported(FLAG_LAYOUT_TREE);
	}

	/**
	 * Returns a dialog settings object for this search result page. There will be
	 * one dialog settings object per search result page id.
	 *
	 * @return the dialog settings for this search result page
	 * @see AbstractTextSearchViewPage#getID()
	 */
	protected IDialogSettings getSettings() {
		IDialogSettings parent = PlatformUI
				.getDialogSettingsProvider(FrameworkUtil.getBundle(AbstractTextSearchViewPage.class))
				.getDialogSettings();
		IDialogSettings settings = parent.getSection(getID());
		if (settings == null)
			settings = parent.addNewSection(getID());
		return settings;
	}

	@Override
	public void setID(String id) {
		fId = id;
	}

	@Override
	public String getID() {
		return fId;
	}

	@Override
	public String getLabel() {
		AbstractTextSearchResult result= getInput();
		if (result == null)
			return ""; //$NON-NLS-1$
		return result.getLabel();
	}

	/**
	 * Opens an editor on the given element and selects the given range of text. If a search results
	 * implements a <code>IFileMatchAdapter</code>, match locations will be tracked and the current
	 * match range will be passed into this method.
	 *
	 * @param match the match to show
	 * @param currentOffset the current start offset of the match
	 * @param currentLength the current length of the selection
	 * @throws PartInitException if an editor can't be opened
	 *
	 * @see org.eclipse.core.filebuffers.ITextFileBufferManager
	 * @see IFileMatchAdapter
	 * @deprecated Use {@link #showMatch(Match, int, int, boolean)} instead
	 */
	@Deprecated
	protected void showMatch(Match match, int currentOffset, int currentLength) throws PartInitException {
	}

	/**
	 * Opens an editor on the given element and selects the given range of text.
	 * If a search results implements a <code>IFileMatchAdapter</code>, match
	 * locations will be tracked and the current match range will be passed into
	 * this method.
	 * If the <code>activate</code> parameter is <code>true</code> the opened editor
	 * should have be activated. Otherwise the focus should not be changed.
	 *
	 * @param match
	 *            the match to show
	 * @param currentOffset
	 *            the current start offset of the match
	 * @param currentLength
	 *            the current length of the selection
	 * @param activate
	 * 			  whether to activate the editor.
	 * @throws PartInitException
	 *             if an editor can't be opened
	 *
	 * @see org.eclipse.core.filebuffers.ITextFileBufferManager
	 * @see IFileMatchAdapter
	 */
	protected void showMatch(Match match, int currentOffset, int currentLength, boolean activate) throws PartInitException {
		showMatch(match, currentOffset, currentLength);
	}

	/**
	 * Opens an editor on the given file resource and tries to select the given
	 * offset and length.
	 * <p>
	 * If the page already has an editor open on the target object then that
	 * editor is brought to front; otherwise, a new editor is opened. If
	 * <code>activate == true</code> the editor will be activated.
	 * </p>
	 *
	 * @param page
	 *            the workbench page in which the editor will be opened
	 * @param file
	 *            the file to open
	 * @param offset
	 *            the offset to select in the editor
	 * @param length
	 *            the length to select in the editor
	 * @param activate
	 *            if <code>true</code> the editor will be activated
	 * @return an open editor or <code>null</code> if an external editor was
	 *         opened
	 * @throws PartInitException
	 *             if the editor could not be initialized
	 * @see org.eclipse.ui.IWorkbenchPage#openEditor(IEditorInput, String,
	 *      boolean)
	 * @since 3.6
	 */
	protected final IEditorPart openAndSelect(IWorkbenchPage page, IFile file, int offset, int length, boolean activate) throws PartInitException {
		return fEditorOpener.openAndSelect(page, file, offset, length, activate);
	}

	/**
	 * Opens an editor on the given file resource.
	 * <p>
	 * If the page already has an editor open on the target object then that
	 * editor is brought to front; otherwise, a new editor is opened. If
	 * <code>activate == true</code> the editor will be activated.
	 * </p>
	 *
	 * @param page
	 *            the workbench page in which the editor will be opened
	 * @param file
	 *            the file to open
	 * @param activate
	 *            if <code>true</code> the editor will be activated
	 * @return an open editor or <code>null</code> if an external editor was
	 *         opened
	 * @throws PartInitException
	 *             if the editor could not be initialized
	 * @see org.eclipse.ui.IWorkbenchPage#openEditor(IEditorInput, String,
	 *      boolean)
	 * @since 3.6
	 */
	protected final IEditorPart open(IWorkbenchPage page, IFile file, boolean activate) throws PartInitException {
		return fEditorOpener.open(page, file, activate);
	}

	/**
	 * This method is called whenever the set of matches for the given elements
	 * changes. This method is guaranteed to be called in the UI thread. Note
	 * that this notification is asynchronous. i.e. further changes may have
	 * occurred by the time this method is called. They will be described in a
	 * future call.
	 * <p>The changed elements are evaluated by {@link #evaluateChangedElements(Match[], Set)}.</p>
	 *
	 * @param objects
	 *            array of objects that has to be refreshed
	 */
	protected abstract void elementsChanged(Object[] objects);

	/**
	 * This method is called whenever all elements have been removed from the
	 * shown <code>AbstractSearchResult</code>. This method is guaranteed to
	 * be called in the UI thread. Note that this notification is asynchronous.
	 * i.e. further changes may have occurred by the time this method is called.
	 * They will be described in a future call.
	 */
	protected abstract void clear();

	/**
	 * Configures the given viewer. Implementers have to set at least a content
	 * provider and a label provider. This method may be called if the page was
	 * constructed with the flag <code>FLAG_LAYOUT_TREE</code>.
	 *
	 * @param viewer the viewer to be configured
	 */
	protected abstract void configureTreeViewer(TreeViewer viewer);

	/**
	 * Configures the given viewer. Implementers have to set at least a content
	 * provider and a label provider. This method may be called if the page was
	 * constructed with the flag <code>FLAG_LAYOUT_FLAT</code>.
	 *
	 * @param viewer the viewer to be configured
	 */
	protected abstract void configureTableViewer(TableViewer viewer);

	/**
	 * Fills the context menu for this page. Subclasses may override this
	 * method.
	 *
	 * @param mgr the menu manager representing the context menu
	 */
	protected void fillContextMenu(IMenuManager mgr) {
		mgr.appendToGroup(IContextMenuConstants.GROUP_SHOW, fShowNextAction);
		mgr.appendToGroup(IContextMenuConstants.GROUP_SHOW, fShowPreviousAction);
		mgr.appendToGroup(IContextMenuConstants.GROUP_EDIT, fCopyToClipboardAction);
		if (getCurrentMatch() != null)
			mgr.appendToGroup(IContextMenuConstants.GROUP_REMOVE_MATCHES, fRemoveCurrentMatch);
		if (canRemoveMatchesWith(getViewer().getSelection()))
			mgr.appendToGroup(IContextMenuConstants.GROUP_REMOVE_MATCHES, fRemoveSelectedMatches);
		mgr.appendToGroup(IContextMenuConstants.GROUP_REMOVE_MATCHES, fRemoveAllResultsAction);
	}

	/**
	 * Determines whether the provided selection can be used to remove matches from the result.
	 * @param selection the selection to test
	 * @return returns <code>true</code> if the elements in the current selection can be removed.
	 * @since 3.2
	 */
	protected boolean canRemoveMatchesWith(ISelection selection) {
		return !selection.isEmpty();
	}

	@Override
	public void createControl(Composite parent) {
		fQueryListener = createQueryListener();
		fMenu = new MenuManager("#PopUp"); //$NON-NLS-1$
		fMenu.setRemoveAllWhenShown(true);
		fMenu.setParent(getSite().getActionBars().getMenuManager());
		fMenu.addMenuListener(mgr -> {
			SearchView.createContextMenuGroups(mgr);
			fillContextMenu(mgr);
			fViewPart.fillContextMenu(mgr);
		});
		fPagebook = new PageBook(parent, SWT.NULL);
		fPagebook.setLayoutData(new GridData(GridData.FILL_BOTH));
		fBusyLabel = createBusyControl();
		fViewerContainer = new Composite(fPagebook, SWT.NULL);
		fViewerContainer.setLayoutData(new GridData(GridData.FILL_BOTH));
		fViewerContainer.setSize(100, 100);
		fViewerContainer.setLayout(new FillLayout());

		fViewerAdapter= new SelectionProviderAdapter();
		getSite().setSelectionProvider(fViewerAdapter);
		// Register menu
		getSite().registerContextMenu(fViewPart.getViewSite().getId(), fMenu, fViewerAdapter);


		createViewer(fViewerContainer, fCurrentLayout);
		showBusyLabel(fIsBusyShown);
		NewSearchUI.addQueryListener(fQueryListener);

	}

	private Control createBusyControl() {
		Table busyLabel = new Table(fPagebook, SWT.NONE);
		TableItem item = new TableItem(busyLabel, SWT.NONE);
		item.setText(SearchMessages.AbstractTextSearchViewPage_searching_label);
		busyLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
		return busyLabel;
	}

	private synchronized void scheduleUIUpdate() {
		if (!fIsUIUpdateScheduled) {
			fIsUIUpdateScheduled= true;
			new UpdateUIJob().schedule();
		}
	}

	private IQueryListener createQueryListener() {
		return new IQueryListener() {
			@Override
			public void queryAdded(ISearchQuery query) {
				// ignore
			}

			@Override
			public void queryRemoved(ISearchQuery query) {
				// ignore
			}

			@Override
			public void queryStarting(final ISearchQuery query) {
				final Runnable runnable1 = () -> {
					updateBusyLabel();
					AbstractTextSearchResult result = getInput();

					if (result == null || !result.getQuery().equals(query)) {
						return;
					}
					turnOffDecoration();
					scheduleUIUpdate();
				};
				asyncExec(runnable1);
			}

			@Override
			public void queryFinished(final ISearchQuery query) {
				// handle the end of the query in the UIUpdateJob, as ui updates
				// may not be finished here.
				postEnsureSelection();
			}
		};
	}

	/**
	 * Posts a  UI update to make sure an element is selected.
	 * @since 3.2
	 */
	protected void postEnsureSelection() {
		fScheduleEnsureSelection= true;
		scheduleUIUpdate();
	}


	private void updateBusyLabel() {
		AbstractTextSearchResult result = getInput();
		boolean shouldShowBusy = result != null && NewSearchUI.isQueryRunning(result.getQuery()) && result.getMatchCount() == 0;
		if (shouldShowBusy == fIsBusyShown)
			return;
		fIsBusyShown = shouldShowBusy;
		showBusyLabel(fIsBusyShown);
	}

	private void showBusyLabel(boolean shouldShowBusy) {
		if (shouldShowBusy)
			fPagebook.showPage(fBusyLabel);
		else
			fPagebook.showPage(fViewerContainer);
	}

	/**
	 * Determines whether a certain layout is supported by this search result
	 * page.
	 *
	 * @param layout the layout to test for
	 * @return whether the given layout is supported or not
	 *
	 * @see AbstractTextSearchViewPage#AbstractTextSearchViewPage(int)
	 */
	public boolean isLayoutSupported(int layout) {
		return (layout & fSupportedLayouts) == layout;
	}

	/**
	 * Sets the layout of this search result page. The layout must be on of
	 * <code>FLAG_LAYOUT_FLAT</code> or <code>FLAG_LAYOUT_TREE</code> and
	 * it must be one of the values passed during construction of this search
	 * result page.
	 * @param layout the new layout
	 *
	 * @see AbstractTextSearchViewPage#isLayoutSupported(int)
	 */
	public void setLayout(int layout) {
		Assert.isTrue(countBits(layout) == 1);
		Assert.isTrue(isLayoutSupported(layout));
		if (countBits(fSupportedLayouts) < 2)
			return;
		if (fCurrentLayout == layout)
			return;
		fCurrentLayout = layout;
		ISelection selection = fViewer.getSelection();
		disconnectViewer();
		disposeViewer();
		createViewer(fViewerContainer, layout);
		fViewerContainer.layout(true);
		connectViewer(fInput);
		fViewer.setSelection(selection, true);
		getSettings().put(KEY_LAYOUT, layout);
		getViewPart().updateLabel();
	}

	private void disposeViewer() {
		fViewer.removeSelectionChangedListener(fViewerAdapter);
		fViewer.getControl().dispose();
		fViewer = null;
	}

	private void updateLayoutActions() {
		if (fFlatAction != null)
			fFlatAction.setChecked(fCurrentLayout == fFlatAction.getLayout());
		if (fHierarchicalAction != null)
			fHierarchicalAction.setChecked(fCurrentLayout == fHierarchicalAction.getLayout());
	}

	/**
	 * Return the layout this page is currently using.
	 *
	 * @return the layout this page is currently using
	 *
	 * @see #FLAG_LAYOUT_FLAT
	 * @see #FLAG_LAYOUT_TREE
	 */
	public int getLayout() {
		return fCurrentLayout;
	}

	private void createViewer(Composite parent, int layout) {
		if ((layout & FLAG_LAYOUT_FLAT) != 0) {
			TableViewer viewer = createTableViewer(parent);
			fViewer = viewer;
			configureTableViewer(viewer);
		} else if ((layout & FLAG_LAYOUT_TREE) != 0) {
			TreeViewer viewer = createTreeViewer(parent);
			fViewer = viewer;
			configureTreeViewer(viewer);
			fCollapseAllAction.setViewer(viewer);
			fExpandAllAction.setViewer(viewer);
		}

		fCopyToClipboardAction.setViewer(fViewer);
		fSelectAllAction.setViewer(fViewer);

		IToolBarManager tbm = getSite().getActionBars().getToolBarManager();
		tbm.removeAll();
		SearchView.createToolBarGroups(tbm);
		fillToolbar(tbm);
		tbm.update(false);

		new OpenAndLinkWithEditorHelper(fViewer) {

			@Override
			protected void activate(ISelection selection) {
				final int currentMode= OpenStrategy.getOpenMethod();
				try {
					OpenStrategy.setOpenMethod(OpenStrategy.DOUBLE_CLICK);
					handleOpen(new OpenEvent(fViewer, selection));
				} finally {
					OpenStrategy.setOpenMethod(currentMode);
				}
			}

			@Override
			protected void linkToEditor(ISelection selection) {
				// not supported by this part
			}

			@Override
			protected void open(ISelection selection, boolean activate) {
				handleOpen(new OpenEvent(fViewer, selection));
			}

		};

		fViewer.addSelectionChangedListener(event -> {
			fCurrentMatchIndex = -1;
			fRemoveSelectedMatches.setEnabled(canRemoveMatchesWith(event.getSelection()));
		});

		fViewer.addSelectionChangedListener(fViewerAdapter);

		Menu menu = fMenu.createContextMenu(fViewer.getControl());
		fViewer.getControl().setMenu(menu);

		updateLayoutActions();
		getViewPart().updateLabel();
	}

	/**
	 * Creates the tree viewer to be shown on this page. Clients may override
	 * this method.
	 *
	 * @param parent the parent widget
	 * @return returns a newly created <code>TreeViewer</code>.
	 */
	protected TreeViewer createTreeViewer(Composite parent) {
		return new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
	}

	/**
	 * Creates the table viewer to be shown on this page. Clients may override
	 * this method.
	 *
	 * @param parent the parent widget
	 * @return returns a newly created <code>TableViewer</code>
	 */
	protected TableViewer createTableViewer(Composite parent) {
		return new TableViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
	}

	@Override
	public void setFocus() {
		Control control = fViewer.getControl();
		if (control != null && !control.isDisposed())
			control.setFocus();
	}

	@Override
	public Control getControl() {
		return fPagebook;
	}

	@Override
	public void setInput(ISearchResult newSearch, Object viewState) {
		if (newSearch != null && !(newSearch instanceof AbstractTextSearchResult))
			return; // ignore

		AbstractTextSearchResult oldSearch= fInput;
		if (oldSearch != null) {
			disconnectViewer();
			removeFilterActionsFromViewMenu(fFilterActions);
			oldSearch.removeListener(fListener);
			AnnotationManagers.removeSearchResult(getSite().getWorkbenchWindow(), oldSearch);
		}
		fInput= (AbstractTextSearchResult) newSearch;

		if (fInput != null) {
			AnnotationManagers.addSearchResult(getSite().getWorkbenchWindow(), fInput);

			fInput.addListener(fListener);
			connectViewer(fInput);
			if (viewState instanceof ISelection)
				fViewer.setSelection((ISelection) viewState, true);
			else
				navigateNext(true);

			updateBusyLabel();
			turnOffDecoration();
			scheduleUIUpdate();

			fFilterActions= addFilterActionsToViewMenu();
		} else {
			getViewPart().updateLabel();
		}
	}

	private void removeFilterActionsFromViewMenu(IAction[] filterActions) {
		IActionBars bars= getSite().getActionBars();
		IMenuManager menu= bars.getMenuManager();

		if (filterActions != null) {
			for (IAction filterAction : filterActions) {
				menu.remove(filterAction.getId());
			}
		}
		menu.remove(MatchFilterSelectionAction.ACTION_ID);
	}

	private IAction[] addFilterActionsToViewMenu() {
		AbstractTextSearchResult input= getInput();
		if (input == null) {
			return null;
		}

		MatchFilter[] allMatchFilters= input.getAllMatchFilters();
		if (allMatchFilters == null && getElementLimit() == null) {
			return null;
		}

		IActionBars bars= getSite().getActionBars();
		IMenuManager menu= bars.getMenuManager();

		menu.prependToGroup(IContextMenuConstants.GROUP_FILTERING, new MatchFilterSelectionAction(this));

		if (allMatchFilters != null) {
			MatchFilterAction[] actions= new MatchFilterAction[allMatchFilters.length];
			for (int i= allMatchFilters.length - 1; i >= 0; i--) {
				MatchFilterAction filterAction= new MatchFilterAction(this, allMatchFilters[i]);
				actions[i]= filterAction;
				menu.prependToGroup(IContextMenuConstants.GROUP_FILTERING, filterAction);
			}
			return actions;
		}
		return null;
	}

	private void updateFilterActions(IAction[] filterActions) {
		if (filterActions != null) {
			for (IAction curr : filterActions) {
				if (curr instanceof IUpdate) {
					((IUpdate) curr).update();
				}
			}
		}
	}

	@Override
	public Object getUIState() {
		return fViewer.getSelection();
	}

	private void connectViewer(AbstractTextSearchResult search) {
		fViewer.setInput(search);
	}

	private void disconnectViewer() {
		fViewer.setInput(null);
	}

	/**
	 * Returns the viewer currently used in this page.
	 *
	 * @return the currently used viewer or <code>null</code> if none has been
	 *         created yet.
	 */
	protected StructuredViewer getViewer() {
		return fViewer;
	}

	private void showMatch(final Match match, final boolean activateEditor) {
		ISafeRunnable runnable = new ISafeRunnable() {
			@Override
			public void handleException(Throwable exception) {
				if (exception instanceof PartInitException) {
					PartInitException pie = (PartInitException) exception;
					ErrorDialog.openError(getSite().getShell(), SearchMessages.DefaultSearchViewPage_show_match, SearchMessages.DefaultSearchViewPage_error_no_editor, pie.getStatus());
				}
			}

			@Override
			public void run() throws Exception {
				IRegion location= getCurrentMatchLocation(match);
				showMatch(match, location.getOffset(), location.getLength(), activateEditor);
			}
		};
		SafeRunner.run(runnable);
	}

	/**
	 * Returns the currently shown result.
	 *
	 * @return the previously set result or <code>null</code>
	 *
	 * @see AbstractTextSearchViewPage#setInput(ISearchResult, Object)
	 */
	public AbstractTextSearchResult getInput() {
		return fInput;
	}

	/**
	 * Selects the element corresponding to the next match and shows the match
	 * in an editor. Note that this will cycle back to the first match after the
	 * last match.
	 */
	public void gotoNextMatch() {
		gotoNextMatch(false);
	}

	private void gotoNextMatch(boolean activateEditor) {
		fCurrentMatchIndex++;
		Match nextMatch = getCurrentMatch();
		if (nextMatch == null) {
			navigateNext(true);
			fCurrentMatchIndex = 0;
		}
		showCurrentMatch(activateEditor);
	}

	/**
	 * Selects the element corresponding to the previous match and shows the
	 * match in an editor. Note that this will cycle back to the last match
	 * after the first match.
	 */
	public void gotoPreviousMatch() {
		gotoPreviousMatch(false);
	}

	private void gotoPreviousMatch(boolean activateEditor) {
		fCurrentMatchIndex--;
		Match nextMatch = getCurrentMatch();
		if (nextMatch == null) {
			navigateNext(false);
			fCurrentMatchIndex = getDisplayedMatchCount(getFirstSelectedElement()) - 1;
		}
		showCurrentMatch(activateEditor);
	}

	private void navigateNext(boolean forward) {
		INavigate navigator = null;
		if (fViewer instanceof TableViewer) {
			navigator = new TableViewerNavigator((TableViewer) fViewer);
		} else {
			navigator = new TreeViewerNavigator(this, (TreeViewer) fViewer);
		}
		navigator.navigateNext(forward);
	}

	private boolean showCurrentMatch(boolean activateEditor) {
		Match currentMatch = getCurrentMatch();
		if (currentMatch != null) {
			showMatch(currentMatch, activateEditor);
			return true;
		}
		return false;
	}

	/**
	 * Returns the currently selected match.
	 *
	 * @return the selected match or <code>null</code> if none are selected
	 */
	public Match getCurrentMatch() {
		Object element = getFirstSelectedElement();
		if (element != null) {
			Match[] matches = getDisplayedMatches(element);
			if (fCurrentMatchIndex >= 0 && fCurrentMatchIndex < matches.length)
				return matches[fCurrentMatchIndex];
		}
		return null;
	}

	/**
	 * Returns the matches that are currently displayed for the given element.
	 * If {@link AbstractTextSearchResult#getActiveMatchFilters()} is not null, only matches are returned
	 * that are not filtered by the match filters. If {@link AbstractTextSearchResult#getActiveMatchFilters()} is
	 * null all matches of the given element are returned.
	 * Any action operating on the visible matches in the search
	 * result page should use this method to get the matches for a search
	 * result (instead of asking the search result directly).
	 *
	 * @param element
	 *            The element to get the matches for
	 * @return The matches displayed for the given element. If the current input
	 *         of this page is <code>null</code>, an empty array is returned
	 * @see AbstractTextSearchResult#getMatches(Object)
	 */
	public Match[] getDisplayedMatches(Object element) {
		AbstractTextSearchResult result= getInput();
		if (result == null)
			return EMPTY_MATCH_ARRAY;
		Match[] matches= result.getMatches(element);
		if (result.getActiveMatchFilters() == null) // default behaviour: filter state not used, all matches shown
			return matches;

		int count= 0;
		for (int i= 0; i < matches.length; i++) {
			if (matches[i].isFiltered())
				matches[i]= null;
			else
				count++;
		}
		if (count == matches.length)
			return matches;

		Match[] filteredMatches= new Match[count];
		for (int i= 0, k= 0; i < matches.length; i++) {
			if (matches[i] != null)
				filteredMatches[k++]= matches[i];
		}
		return filteredMatches;
	}

	/**
	 * Returns the current location of the match. This takes possible
	 * modifications of the file into account. Therefore the result may
	 * differ from the position information that can be obtained directly
	 * off the match.
	 * @param match the match to get the position for.
	 * @return the current position of the match.
	 *
	 * @since 3.2
	 */
	public IRegion getCurrentMatchLocation(Match match) {
		PositionTracker tracker= InternalSearchUI.getInstance().getPositionTracker();

		int offset, length;
		Position pos= tracker.getCurrentPosition(match);
		if (pos == null) {
			offset= match.getOffset();
			length= match.getLength();
		} else {
			offset= pos.getOffset();
			length= pos.getLength();
		}
		return new Region(offset, length);
	}

	/**
	 * Returns the number of matches that are currently displayed for the given
	 * element. If {@link AbstractTextSearchResult#getActiveMatchFilters()} is not null, only matches
	 * are returned that are not filtered by the match filters.
	 * Any action operating on the visible matches in the
	 * search result page should use this method to get the match count for a
	 * search result (instead of asking the search result directly).
	 *
	 * @param element
	 *            The element to get the matches for
	 * @return The number of matches displayed for the given element. If the
	 *         current input of this page is <code>null</code>, 0 is
	 *         returned
	 * @see AbstractTextSearchResult#getMatchCount(Object)
	 */
	public int getDisplayedMatchCount(Object element) {
		AbstractTextSearchResult result= getInput();
		if (result == null)
			return 0;
		if (result.getActiveMatchFilters() == null) // default behaviour: filter state not used, all matches shown
			return result.getMatchCount(element);

		int count= 0;
		Match[] matches= result.getMatches(element);
		for (Match match : matches) {
			if (!match.isFiltered()) {
				count++;
			}
		}
		return count;
	}

	private Object getFirstSelectedElement() {
		IStructuredSelection selection = fViewer.getStructuredSelection();
		if (selection.size() > 0)
			return selection.getFirstElement();
		return null;
	}

	@Override
	public void dispose() {
		AbstractTextSearchResult oldSearch = getInput();
		if (oldSearch != null)
			AnnotationManagers.removeSearchResult(getSite().getWorkbenchWindow(), oldSearch);
		super.dispose();
		NewSearchUI.removeQueryListener(fQueryListener);
	}

	@Override
	public void init(IPageSite pageSite) {
		super.init(pageSite);
		IMenuManager menuManager= pageSite.getActionBars().getMenuManager();
		addLayoutActions(menuManager);
		initActionDefinitionIDs();
		menuManager.updateAll(true);
		pageSite.getActionBars().updateActionBars();
	}

	private void initActionDefinitionIDs() {
		fCopyToClipboardAction.setActionDefinitionId(IWorkbenchCommandConstants.EDIT_COPY);
		fRemoveSelectedMatches.setActionDefinitionId(IWorkbenchCommandConstants.EDIT_DELETE);
		fShowNextAction.setActionDefinitionId(IWorkbenchCommandConstants.NAVIGATE_NEXT);
		fShowPreviousAction.setActionDefinitionId(IWorkbenchCommandConstants.NAVIGATE_PREVIOUS);
		fSelectAllAction.setActionDefinitionId(IWorkbenchCommandConstants.EDIT_SELECT_ALL);
	}

	/**
	 * Fills the toolbar contribution for this page. Subclasses may override
	 * this method.
	 *
	 * @param tbm the tool bar manager representing the view's toolbar
	 */
	protected void fillToolbar(IToolBarManager tbm) {
		tbm.appendToGroup(IContextMenuConstants.GROUP_SHOW, fShowNextAction);
		tbm.appendToGroup(IContextMenuConstants.GROUP_SHOW, fShowPreviousAction);
		tbm.appendToGroup(IContextMenuConstants.GROUP_REMOVE_MATCHES, fRemoveSelectedMatches);
		tbm.appendToGroup(IContextMenuConstants.GROUP_REMOVE_MATCHES, fRemoveAllResultsAction);
		IActionBars actionBars = getSite().getActionBars();
		if (actionBars != null) {
			actionBars.setGlobalActionHandler(ActionFactory.NEXT.getId(), fShowNextAction);
			actionBars.setGlobalActionHandler(ActionFactory.PREVIOUS.getId(), fShowPreviousAction);
			actionBars.setGlobalActionHandler(ActionFactory.DELETE.getId(), fRemoveSelectedMatches);
			actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), fCopyToClipboardAction);
			actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), fSelectAllAction);
		}
		if (getLayout() == FLAG_LAYOUT_TREE) {
			tbm.appendToGroup(IContextMenuConstants.GROUP_VIEWER_SETUP, fExpandAllAction);
			tbm.appendToGroup(IContextMenuConstants.GROUP_VIEWER_SETUP, fCollapseAllAction);
		}
	}

	private void addLayoutActions(IMenuManager menuManager) {
		if (fFlatAction != null)
			menuManager.appendToGroup(IContextMenuConstants.GROUP_VIEWER_SETUP, fFlatAction);
		if (fHierarchicalAction != null)
			menuManager.appendToGroup(IContextMenuConstants.GROUP_VIEWER_SETUP, fHierarchicalAction);
	}

	/**
	 * Sets the view part
	 * @param part View part to set
	 */
	@Override
	public void setViewPart(ISearchResultViewPart part) {
		fViewPart = part;
	}

	/**
	 * Returns the view part set with
	 * <code>setViewPart(ISearchResultViewPart)</code>.
	 *
	 * @return The view part or <code>null</code> if the view part hasn't been
	 *         set yet (or set to null).
	 */
	protected ISearchResultViewPart getViewPart() {
		return fViewPart;
	}

	// multi-threaded update handling.

	/**
	 * Handles a search result event for the current search result.
	 * @param e the event to handle
	 *
	 * @since 3.2
	 */
	protected void handleSearchResultChanged(final SearchResultEvent e) {
		if (e instanceof MatchEvent) {
			postUpdate(((MatchEvent) e).getMatches());
		} else if (e instanceof RemoveAllEvent) {
			postClear();
		} else if (e instanceof FilterUpdateEvent) {
			postUpdate(((FilterUpdateEvent) e).getUpdatedMatches());
			updateFilterActions(fFilterActions);
		}
	}

	/**
	 * Evaluates the elements to that are later passed to {@link #elementsChanged(Object[])}. By default
	 * the element to change are the elements received by ({@link Match#getElement()}). Client implementations
	 * can modify this behavior.
	 *
	 * @param matches the matches that were added or removed
	 * @param changedElements the set that collects the elements to change. Clients should only add elements to the set.
	 * @since 3.4
	 */
	protected void evaluateChangedElements(Match[] matches, Set<Object> changedElements) {
		for (Match match : matches) {
			changedElements.add(match.getElement());
		}
	}

	private void postUpdate(Match[] matches) {
		HashSet<Object> collect = new HashSet<>();
		// for compatibility we do not pass the "fBatchedUpdates" directly:
		evaluateChangedElements(matches, collect);
		// nulls are forbidden in concurrent datastructures:
		collect.removeIf(Objects::isNull);
		fBatchedUpdates.addAll(collect);
		scheduleUIUpdate(); // still synchronized
	}

	private void runBatchedUpdates() {
		Collection<Object> drain = new ArrayList<>();
		fBatchedUpdates.drainTo(drain);
		elementsChanged(drain.toArray());
		updateBusyLabel();
	}

	private void postClear() {
		fBatchedClearAll= true;
		fBatchedUpdates.clear();
		scheduleUIUpdate(); // still synchronized
	}

	private boolean hasMoreUpdates() {
		return fBatchedClearAll || !fBatchedUpdates.isEmpty();
	}

	private boolean isQueryRunning() {
		AbstractTextSearchResult result= getInput();
		if (result != null) {
			return NewSearchUI.isQueryRunning(result.getQuery());
		}
		return false;
	}

	private void runBatchedClear() {
		synchronized(this) {
			if (!fBatchedClearAll) {
				return;
			}
			fBatchedClearAll= false;
			updateBusyLabel();
		}
		getViewPart().updateLabel();
		clear();
	}

	private void asyncExec(final Runnable runnable) {
		final Control control = getControl();
		if (control != null && !control.isDisposed()) {
			Display currentDisplay = Display.getCurrent();
			if (currentDisplay == null || !currentDisplay.equals(control.getDisplay()))
				// meaning we're not executing on the display thread of the
				// control
				control.getDisplay().asyncExec(() -> {
					if (!control.isDisposed())
						runnable.run();
				});
			else
				runnable.run();
		}
	}

	/**
	 * {@inheritDoc}
	 * Subclasses may extend this method.
	 */
	@Override
	public void restoreState(IMemento memento) {
		if (countBits(fSupportedLayouts) > 1) {
			try {
				fCurrentLayout = getSettings().getInt(KEY_LAYOUT);
				// workaround because the saved value may be 0
				if (fCurrentLayout == 0)
					initLayout();
			} catch (NumberFormatException e) {
				// ignore, signals no value stored.
			}
			if (memento != null) {
				Integer layout = memento.getInteger(KEY_LAYOUT);
				if (layout != null) {
					fCurrentLayout = layout.intValue();
					// workaround because the saved value may be 0
					if (fCurrentLayout == 0)
						initLayout();
				}
			}
		}
	}

	@Override
	public void saveState(IMemento memento) {
		if (countBits(fSupportedLayouts) > 1) {
			memento.putInteger(KEY_LAYOUT, fCurrentLayout);
		}
	}

	/**
	 * Note: this is internal API and should not be called from clients outside
	 * of the search plug-in.
	 * <p>
	 * Removes the currently selected match. Does nothing if no match is
	 * selected.
	 * </p>
	 *
	 * @noreference This method is not intended to be referenced by clients.
	 */
	public void internalRemoveSelected() {
		AbstractTextSearchResult result = getInput();
		if (result == null)
			return;
		StructuredViewer viewer = getViewer();
		IStructuredSelection selection = viewer.getStructuredSelection();

		HashSet<Match> set = new HashSet<>();
		if (viewer instanceof TreeViewer) {
			ITreeContentProvider cp = (ITreeContentProvider) viewer.getContentProvider();
			collectAllMatchesBelow(result, set, cp, selection.toArray());
		} else {
			collectAllMatches(set, selection.toArray());
		}
		navigateNext(true);

		Match[] matches = new Match[set.size()];
		set.toArray(matches);
		result.removeMatches(matches);
	}

	private void collectAllMatches(HashSet<Match> set, Object[] elements) {
		for (Object element : elements) {
			Match[] matches = getDisplayedMatches(element);
			Collections.addAll(set, matches);
		}
	}

	private void collectAllMatchesBelow(AbstractTextSearchResult result, Set<Match> set, ITreeContentProvider cp, Object[] elements) {
		for (Object element : elements) {
			Match[] matches = getDisplayedMatches(element);
			Collections.addAll(set, matches);
			Object[] children = cp.getChildren(element);
			collectAllMatchesBelow(result, set, cp, children);
		}
	}

	private void turnOffDecoration() {
		IBaseLabelProvider lp= fViewer.getLabelProvider();
		if (lp instanceof DecoratingLabelProvider) {
			((DecoratingLabelProvider)lp).setLabelDecorator(null);
		}
	}

	private void turnOnDecoration() {
		IBaseLabelProvider lp= fViewer.getLabelProvider();
		if (lp instanceof DecoratingLabelProvider) {
			((DecoratingLabelProvider)lp).setLabelDecorator(PlatformUI.getWorkbench().getDecoratorManager().getLabelDecorator());

		}
	}

	/**
	 * <p>This method is called when the search page gets an 'open' event from its
	 * underlying viewer (for example on double click). The default
	 * implementation will open the first match on any element that has matches.
	 * If the element to be opened is an inner node in the tree layout, the node
	 * will be expanded if it's collapsed and vice versa. Subclasses are allowed
	 * to override this method.
	 * </p>
	 * @param event
	 *            the event sent for the currently shown viewer
	 *
	 * @see IOpenListener
	 */
	protected void handleOpen(OpenEvent event) {
		Viewer viewer= event.getViewer();
		boolean hasCurrentMatch = showCurrentMatch(OpenStrategy.activateOnOpen());
		ISelection sel= event.getSelection();
		if (viewer instanceof TreeViewer && sel instanceof IStructuredSelection) {
			IStructuredSelection selection= (IStructuredSelection) sel;
			TreeViewer tv = (TreeViewer) getViewer();
			Object element = selection.getFirstElement();
			if (element != null) {
				if (!hasCurrentMatch && getDisplayedMatchCount(element) > 0)
					gotoNextMatch(OpenStrategy.activateOnOpen());
				else
					tv.setExpandedState(element, !tv.getExpandedState(element));
			}
			return;
		} else if (!hasCurrentMatch) {
			gotoNextMatch(OpenStrategy.activateOnOpen());
		}
	}

	/**
	 * Sets the maximal number of top level elements to be shown in a viewer. If
	 * <code>null</code> is set, the view page does not support to limit the
	 * elements and will not provide UI to configure it. If a non-null value is
	 * set, configuration UI will be provided. The limit value must be a
	 * positive number or <code>-1</code> to not limit top level element. If
	 * enabled, the element limit has to be enforced by the content provider
	 * that is implemented by the client. The view page just manages the value
	 * and configuration.
	 *
	 * @param limit
	 *            the element limit. Valid values are:
	 *            <ul>
	 *            <li><code>null</code> to not limit and not provide
	 *            configuration UI</li>
	 *            <li><code>-1</code> to not limit and provide configuration
	 *            UI</li>
	 *            <li><code>positive integer</code> to limit by the given value
	 *            and provide configuration UI</li>
	 *            </ul>
	 *
	 * @since 3.3
	 */
	public void setElementLimit(Integer limit) {
		fElementLimit= limit;

		if (fViewer != null) {
			fViewer.refresh();
		}
		if (fViewPart != null) {
			fViewPart.updateLabel();
		}
	}

	/**
	 * Gets the maximal number of top level elements to be shown in a viewer.
	 * <code>null</code> means the view page does not limit the elements and
	 * will not provide UI to configure it. If a non-null value is set,
	 * configuration UI will be provided. The limit value must be a positive
	 * number or <code>-1</code> to not limit top level element.
	 *
	 * @return returns the element limit. Valid values are:
	 *         <ul>
	 *         <li><code>null</code> to not limit and not provide configuration
	 *         UI (default value)</li>
	 *         <li><code>-1</code> to not limit and provide configuration
	 *         UI</li>
	 *         <li><code>positive integer</code> to limit by the given value and
	 *         provide configuration UI</li>
	 *         </ul>
	 *
	 * @since 3.3
	 */
	public Integer getElementLimit() {
		return fElementLimit;
	}


}
