/*******************************************************************************
 * Copyright (c) 2007, 2018 Dakshinamurthy Karra, 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:
 *     Dakshinamurthy Karra (Jalian Systems) - Templates View - https://bugs.eclipse.org/bugs/show_bug.cgi?id=69581
 *     Piotr Maj <pm@jcake.com> - no access to template store and current selection - https://bugs.eclipse.org/bugs/show_bug.cgi?id=296439
 *******************************************************************************/
package org.eclipse.ui.texteditor.templates;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import com.ibm.icu.text.Collator;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.ViewForm;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSourceAdapter;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

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.action.Separator;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.layout.PixelConverter;
import org.eclipse.jface.layout.TreeColumnLayout;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.ColumnPixelData;
import org.eclipse.jface.viewers.IPostSelectionProvider;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.window.Window;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.text.source.SourceViewerConfiguration;
import org.eclipse.jface.text.templates.ContextTypeRegistry;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.text.templates.TemplateContextType;
import org.eclipse.jface.text.templates.persistence.TemplatePersistenceData;
import org.eclipse.jface.text.templates.persistence.TemplateStore;

import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.IWorkbenchCommandConstants;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.dialogs.PreferencesUtil;
import org.eclipse.ui.internal.texteditor.NLSUtility;
import org.eclipse.ui.internal.texteditor.TextEditorPlugin;
import org.eclipse.ui.part.Page;

import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.ITextEditorActionConstants;
import org.eclipse.ui.texteditor.ITextEditorExtension2;
import org.eclipse.ui.texteditor.templates.TemplatePreferencePage.EditTemplateDialog;


/**
 * Abstract default implementation for {@link ITemplatesPage}.
 * <p>
 * Clients who are defining an editor may elect to provide a corresponding
 * templates page. This templates page will be presented to the user via the
 * Templates View (the user decides whether their workbench window contains this
 * view) whenever that editor is active. This class should be subclassed by
 * clients.
 * </p>
 * <p>
 * Internally, a AbstractTemplatesPage uses the template store to display different
 * categories. A link to editor mode on the templates page allows to filtering
 * of the categories to only that are supported in this context.
 * </p>
 *
 * @since 3.4
 */
public abstract class AbstractTemplatesPage extends Page implements ITemplatesPage, ITemplatesPageExtension {

	/**
	 * Sashform size
	 */
	private static final String SASH_SIZE_PREF_ID= TextEditorPlugin.PLUGIN_ID
			+ ".templates.templatesPage.sashSize"; //$NON-NLS-1$
	/**
	 * Tree columns widths
	 */
	private static final String COLUMN_NAME_WIDTH_PREF_ID= TextEditorPlugin.PLUGIN_ID
			+ ".templates.templatesPage.nameWidth"; //$NON-NLS-1$
	private static final String COLUMN_DESCRIPTION_WIDTH_PREF_ID= TextEditorPlugin.PLUGIN_ID
			+ ".templates.templatesPage.descriptionWidth"; //$NON-NLS-1$
	/**
	 * Link to editor action setting
	 */
	private static final String LINK_ACTION_PREF_ID= TextEditorPlugin.PLUGIN_ID
			+ ".templates.templatesPage.linkAction"; //$NON-NLS-1$

	/**
	 * Context expand/collapse setting prefix
	 */
	private static final String CONTEXT_COLLAPSE_PREF_ID= TextEditorPlugin.PLUGIN_ID
			+ "templates.templatesPage.context.expand."; //$NON-NLS-1$

	/**
	 * The ID for the popup menu for this templates page
	 */
	private static final String POPUP_MENU_ID= "org.eclipse.ui.texteditor.templates.PopupMenu"; //$NON-NLS-1$


	/**
	 * A post selection changed listener for the editor. Depending on the caret
	 * position updates the templates
	 */
	private final class SelectionChangedListener implements ISelectionChangedListener {
		@Override
		public void selectionChanged(SelectionChangedEvent event) {
			String[] contextTypes= getEditorContextTypeIds();
			if (needUpdate(contextTypes)) {
				fCurrentContextTypeIds= contextTypes;
				updateContextTypes(fCurrentContextTypeIds);
				return;
			}
		}

		/**
		 * Check whether an update of the AbstractTemplatesPage is needed
		 *
		 * @param contextTypes the context types
		 * @return true if update is needed
		 */
		private boolean needUpdate(String[] contextTypes) {
			return fCurrentContextTypeIds == null
					|| fCurrentContextTypeIds.length != contextTypes.length
					|| contextTypeChanged(contextTypes);
		}

		/**
		 * Check whether there is any change in the context types needed
		 *
		 * @param contextTypes the context types
		 * @return true if any of the context types changed
		 */
		private boolean contextTypeChanged(String[] contextTypes) {
			for (int i= 0; i < contextTypes.length; i++) {
				if (!contextTypes[i].equals(fCurrentContextTypeIds[i]))
					return true;
			}
			return false;
		}
	}


	/**
	 * Drop support for the editor linked to this page. When a user drops a
	 * template into the active editor, the template is applied at the drop
	 * position.
	 */
	private final class EditorDropTargetListener extends DropTargetAdapter {
		@Override
		public void dragEnter(DropTargetEvent event) {
			if (!TemplatesTransfer.getInstance().isSupportedType(event.currentDataType))
				return;

			event.detail= DND.DROP_COPY;
		}

		@Override
		public void dragOperationChanged(DropTargetEvent event) {
			if (!TemplatesTransfer.getInstance().isSupportedType(event.currentDataType))
				return;

			event.detail= DND.DROP_COPY;
		}

		@Override
		public void dragOver(DropTargetEvent event) {
			if (!TemplatesTransfer.getInstance().isSupportedType(event.currentDataType))
				return;

			event.feedback |= DND.FEEDBACK_SCROLL | DND.FEEDBACK_SELECT;
			event.detail= DND.DROP_NONE;
			TemplatePersistenceData[] selectedTemplates= getSelectedTemplates();
			if (fTextEditor instanceof ITextEditorExtension2 && ((ITextEditorExtension2)fTextEditor).isEditorInputModifiable() && selectedTemplates.length == 1 &&
					isTemplateValidAtLocation(selectedTemplates[0].getTemplate(), new Point(event.x, event.y)))
				event.detail= DND.DROP_COPY;
		}

		@Override
		public void drop(DropTargetEvent event) {
			if (!TemplatesTransfer.getInstance().isSupportedType(event.currentDataType))
				return;

			TemplatePersistenceData[] selectedTemplates= getSelectedTemplates();
			insertTemplate(selectedTemplates[0].getTemplate());
			// The highlight of the item is removed once the drop happens -
			// restore it
			fTreeViewer.setSelection(new StructuredSelection(selectedTemplates), true);
		}

	}


	/**
	 * Comparator for the viewer. Sorts the templates by name and then
	 * description and context types by names.
	 */
	private static final class TemplateViewerComparator extends ViewerComparator {
		@Override
		public int compare(Viewer viewer, Object object1, Object object2) {
			if ((object1 instanceof TemplatePersistenceData)
					&& (object2 instanceof TemplatePersistenceData)) {
				Template left= ((TemplatePersistenceData) object1).getTemplate();
				Template right= ((TemplatePersistenceData) object2).getTemplate();
				int result= Collator.getInstance().compare(left.getName(), right.getName());
				if (result != 0)
					return result;
				return Collator.getInstance()
						.compare(left.getDescription(), right.getDescription());
			}
			if ((object1 instanceof TemplateContextType)
					&& (object2 instanceof TemplateContextType)) {
				return Collator.getInstance().compare(((TemplateContextType) object1).getName(),
						((TemplateContextType) object2).getName());
			}
			return super.compare(viewer, object1, object2);
		}

		@Override
		public boolean isSorterProperty(Object element, String property) {
			return false;
		}
	}


	/**
	 * Label provider for templates.
	 */
	private final class TemplateLabelProvider extends LabelProvider implements ITableLabelProvider {

		@Override
		public Image getColumnImage(Object element, int columnIndex) {
			if (columnIndex != 0)
				return null;
			if (element instanceof TemplateContextType)
				return TemplatesPageImages.get(TemplatesPageImages.IMG_OBJ_CONTEXT);
			return AbstractTemplatesPage.this.getImage(((TemplatePersistenceData) element).getTemplate());
		}

		@Override
		public String getColumnText(Object element, int columnIndex) {
			if (element instanceof TemplatePersistenceData)
				return getTemplateColumnText((TemplatePersistenceData) element, columnIndex);
			return getContextColumnText((TemplateContextType) element, columnIndex);
		}

		private String getTemplateColumnText(TemplatePersistenceData data, int columnIndex) {
			switch (columnIndex) {
			case 0:
				return data.getTemplate().getName();
			case 1:
				return data.getTemplate().getDescription();
			default:
				return ""; //$NON-NLS-1$
			}
		}

		private String getContextColumnText(TemplateContextType contextType, int columnIndex) {
			switch (columnIndex) {
			case 0:
				return contextType.getName();
			default:
				return ""; //$NON-NLS-1$
			}
		}
	}


	/**
	 * Content provider for templates. Provides all the enabled templates
	 * defined for this editor.
	 */
	private final class TemplatesContentProvider implements ITreeContentProvider {

		@Override
		public Object[] getChildren(Object parentElement) {
			if (parentElement instanceof TemplatePersistenceData)
				return new Object[0];
			else if (parentElement instanceof TemplateContextType) {
				TemplateContextType contextType= (TemplateContextType) parentElement;
				return getTemplates(contextType.getId());
			}
			return null;
		}

		private TemplatePersistenceData[] getTemplates(String contextId) {
			List<TemplatePersistenceData> templateList= new ArrayList<>();
			TemplatePersistenceData[] datas= getTemplateStore().getTemplateData(false);
			for (int i= 0; i < datas.length; i++) {
				if (datas[i].isEnabled() && datas[i].getTemplate().getContextTypeId().equals(contextId))
					templateList.add(datas[i]);
			}
			return templateList
					.toArray(new TemplatePersistenceData[templateList.size()]);
		}

		@Override
		public Object getParent(Object element) {
			if (element instanceof TemplatePersistenceData) {
				TemplatePersistenceData templateData= (TemplatePersistenceData) element;
				return getContextTypeRegistry().getContextType(
						templateData.getTemplate().getContextTypeId());
			}
			return null;
		}

		@Override
		public boolean hasChildren(Object parentElement) {
			if (parentElement instanceof TemplatePersistenceData)
				return false;
			else if (parentElement instanceof TemplateContextType) {
				String contextId= ((TemplateContextType) parentElement).getId();

				TemplatePersistenceData[] datas= getTemplateStore().getTemplateData(false);
				if (datas.length <= 0)
					return false;

				for (int i= 0; i < datas.length; i++) {
					if (datas[i].isEnabled() && datas[i].getTemplate().getContextTypeId().equals(contextId))
						return true;
				}
				return false;
			}
			return false;
		}

		@Override
		public Object[] getElements(Object inputElement) {
			List<TemplateContextType> contextTypes= new ArrayList<>();

			for (Iterator<TemplateContextType> iterator= getContextTypeRegistry().contextTypes(); iterator.hasNext();) {
				TemplateContextType contextType= iterator.next();
				if (!fLinkWithEditorAction.isChecked() || isActiveContext(contextType))
					contextTypes.add(contextType);
			}
			return contextTypes.toArray(new TemplateContextType[contextTypes.size()]);
		}

		private boolean isActiveContext(TemplateContextType contextType) {
			return fActiveTypes == null || fActiveTypes.contains(contextType.getId());
		}

		@Override
		public void dispose() {
		}

		@Override
		public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
		}
	}


	/** The text editor. */
	private final ITextEditor fTextEditor;
	/** The source viewer attached to this editor. */
	private final ISourceViewer fViewer;

	/* Listener to monitor changes to template store */
	private IPropertyChangeListener fTemplateChangeListener;

	/* Control for this pagebook view */
	private SashForm fControl;

	/* Actions */
	private Action fInsertAction;
	private Action fAddAction;
	private Action fEditAction;
	private Action fRemoveAction;
	private Action fLinkWithEditorAction;
	private Action fCollapseAllAction;
	private Action fPreferencePageAction;

	/* Clipboard actions */
	private Action fPasteAction;
	private Action fCopyAction;

	/* Current active context types for the editor */
	private List<String> fActiveTypes;

	/* Preference stores */
	private IPreferenceStore fPreferenceStore;

	/* Controls */
	private Tree fTemplatesTree;
	private TreeViewer fTreeViewer;
	private Menu fContextMenu;

	/** Current selection. */
	private TemplatePersistenceData[] fSelectedTemplates= new TemplatePersistenceData[0];

	/** The pattern viewer to be used with this view. */
	private SourceViewer fPatternViewer;

	/* Cached results for avoiding processing while drag-over the editor. */
	private int fCachedOffset;
	private boolean fCachedResult;
	private Point fCachedPosition;

	/** The current context type ids. */
	private String[] fCurrentContextTypeIds;

	/** The selection changed listener to monitor the editor selections. */
	private SelectionChangedListener fSelectionChangedListener;

	/** Paste action support for the editor. */
	private IAction fEditorOldPasteAction;
	private IAction fEditorPasteAction;


	/**
	 * Creates a new templates page.
	 *
	 * @param editor the editor
	 * @param viewer the source viewer
	 */
	protected AbstractTemplatesPage(ITextEditor editor, ISourceViewer viewer) {
		Assert.isLegal(editor != null);
		Assert.isLegal(viewer != null);
		fTextEditor= editor;
		fViewer= viewer;
		setupPreferenceStore();
		setupEditorDropTarget();
		setupSelectionProvider();
		setupPasteOperation();
	}

	@Override
	public void createControl(Composite ancestor) {
		setupActions();

		fControl= new SashForm(ancestor, SWT.VERTICAL);

		createTemplateTree(fControl);
		createPatternForm(fControl);

		hookContextMenu();
		initializeDND();
		updateButtons();

		int sashSize= fPreferenceStore.getInt(SASH_SIZE_PREF_ID);
		fControl.setWeights(new int[] { sashSize, 100 - sashSize });
		fTemplateChangeListener = event -> getShell().getDisplay().asyncExec(() -> refresh());
		getTemplatePreferenceStore().addPropertyChangeListener(fTemplateChangeListener);
		updateContextTypes(getEditorContextTypeIds());
	}

	@Override
	public void setFocus() {
	}

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

	@Override
	public void dispose() {
		ISelectionProvider selectionProvider= fViewer.getSelectionProvider();
		if (selectionProvider instanceof IPostSelectionProvider)
			((IPostSelectionProvider) selectionProvider).removePostSelectionChangedListener(fSelectionChangedListener);
		else
			selectionProvider.removeSelectionChangedListener(fSelectionChangedListener);
		fTextEditor.setAction(ITextEditorActionConstants.PASTE, fEditorOldPasteAction);
		if (fContextMenu != null && !fContextMenu.isDisposed())
			fContextMenu.dispose();
		if (fTemplateChangeListener != null)
			getTemplatePreferenceStore().removePropertyChangeListener(fTemplateChangeListener);
		super.dispose();
	}

	/**
	 * Returns the shell in which this page is displayed.
	 *
	 * @return the shell
	 */
	private Shell getShell() {
		return getSite().getShell();
	}

	/**
	 * Returns the image to be used for the given template.
	 * <p>
	 * Clients can override to provide a different image.</p>
	 *
	 * @param template the template
	 * @return the image, must not be disposed
	 */
	protected Image getImage(Template template) {
		return TemplatesPageImages.get(TemplatesPageImages.IMG_OBJ_TEMPLATE);
	}

	/**
	 * Creates and opens a dialog to edit the given template.
	 * <p>
	 * Subclasses may override this method to provide a custom dialog.
	 * </p>
	 *
	 * @param template
	 *            the template being edited
	 * @param edit
	 *            <code>true</code> if the dialog allows editing
	 * @param isNameModifiable
	 *            <code>true</code> if the template name may be modified
	 * @return the created or modified template, or <code>null</code> if the
	 *         editing failed
	 */
	protected Template editTemplate(Template template, boolean edit, boolean isNameModifiable) {
		EditTemplateDialog dialog= new EditTemplateDialog(getShell(), template, edit, isNameModifiable, getContextTypeRegistry());
		if (dialog.open() == Window.OK)
			return dialog.getTemplate();
		return null;
	}

	/**
	 * Update the pattern viewer to show the current template.
	 * <p>
	 * Subclasses can extend this method to update their own pattern viewer.
	 * </p>
	 *
	 * @param template the template
	 */
	protected void updatePatternViewer(Template template) {
		String pattern= template != null ? template.getPattern() : ""; //$NON-NLS-1$
		fPatternViewer.getDocument().set(pattern);
	}

	/**
	 * Creates, configures and returns a source viewer to present the template
	 * pattern on the templates page.
	 * <p>
	 * Clients may override to provide a custom source viewer featuring e.g. syntax coloring.</p>
	 *
	 * @param parent the parent control
	 * @return a configured source viewer
	 */
	protected SourceViewer createPatternViewer(Composite parent) {
		SourceViewer viewer= new SourceViewer(parent, null, null, false, SWT.V_SCROLL | SWT.H_SCROLL);
		SourceViewerConfiguration configuration= new SourceViewerConfiguration();
		viewer.configure(configuration);
		IDocument document= new Document();
		viewer.setDocument(document);
		viewer.setEditable(false);
		return viewer;
	}

	/**
	 * Returns the pattern viewer created by createPatternViewer()
	 *
	 * @return the pattern viewer
	 */
	protected final SourceViewer getPatternViewer() {
		return fPatternViewer;
	}

	/**
	 * The caret position in the editor has moved into a new context type. It is
	 * the subclasses responsibility to see that this is called only when needed
	 * by keeping track of editor contents (eg. partitions).
	 *
	 * @param ids the ids
	 */
	private void updateContextTypes(String[] ids) {
		fActiveTypes= Arrays.asList(ids);
		if (fLinkWithEditorAction != null && fLinkWithEditorAction.isChecked())
			refresh();
	}

	/**
	 * Inserts the given template into the editor.
	 *
	 * @param template the template
	 * @param document the document
	 */
	abstract protected void insertTemplate(Template template, IDocument document);

	/**
	 * Returns the context type registry used in this page.
	 *
	 * @return the context type registry
	 */
	abstract protected ContextTypeRegistry getContextTypeRegistry();

	/**
	 * Returns the template store used in this page.
	 *
	 * @return the template store
	 * @since 3.6 public, before it was protected
	 */
	@Override
	abstract public TemplateStore getTemplateStore();

	/**
	 * Returns the preference store used to create the template store returned by
	 * {@link AbstractTemplatesPage#getTemplateStore()}.
	 *
	 * @return the preference store
	 */
	abstract protected IPreferenceStore getTemplatePreferenceStore();

	/**
	 * Returns the Template preference page id to be used by this template page.
	 *
	 * @return id the preference page if or <code>null</code> if none exists
	 */
	abstract protected String getPreferencePageId();

	/**
	 * Returns the context type ids supported at the given document offset.
	 *
	 * @param document the document
	 * @param offset the offset
	 * @return an array of supported context ids
	 */
	protected abstract String[] getContextTypeIds(IDocument document, int offset);

	/**
	 * Returns the context type ids supported at the current
	 * caret position of the editor.
	 *
	 * @return an array with the the supported context type ids
	 */
	private String[] getEditorContextTypeIds() {
		Point selectedRange=fViewer.getSelectedRange();
		int offset= selectedRange.x + selectedRange.y;
		IDocument document= fTextEditor.getDocumentProvider().getDocument(fTextEditor.getEditorInput());
		return getContextTypeIds(document, offset);
	}

	/**
	 * Checks whether the given template is valid for the document at the given
	 * offset and length.
	 *
	 * @param document the document
	 * @param template the template
	 * @param offset the offset
	 * @param length the length
	 * @return <code>true</code> if the template is valid
	 */
	protected abstract boolean isValidTemplate(IDocument document, Template template, int offset, int length);

	/**
	 * Setup the preference store
	 */
	private void setupPreferenceStore() {
		fPreferenceStore= TextEditorPlugin.getDefault().getPreferenceStore();
		fPreferenceStore.setDefault(LINK_ACTION_PREF_ID, true);
		fPreferenceStore.setDefault(SASH_SIZE_PREF_ID, 80);
	}

	/**
	 * Setup the paste operation
	 *
	 * We get the editors Paste operation and sets up a new operation that
	 * checks for the clipboard contents for {@link TemplatesTransfer} data.
	 */
	private void setupPasteOperation() {
		fEditorOldPasteAction= fTextEditor.getAction(ITextEditorActionConstants.PASTE);
		fEditorPasteAction= new Action(TemplatesMessages.TemplatesPage_paste) {
			@Override
			public void run() {
				Clipboard clipboard= new Clipboard(getShell().getDisplay());
				try {
					Template template= getTemplateFromClipboard(clipboard);
					if (template != null)
						insertTemplate(template);
					else
						fEditorOldPasteAction.run();
				} finally {
					clipboard.dispose();
				}
			}

			@Override
			public void runWithEvent(Event event) {
				run();
			}

			/**
			 * Convert the clipboard contents into a template
			 *
			 * @param clipboard the clipboard
			 * @return the template or null if contents are not valid
			 */
			private Template getTemplateFromClipboard(Clipboard clipboard) {
				TemplatePersistenceData[] contents= (TemplatePersistenceData[])clipboard
						.getContents(TemplatesTransfer.getInstance());
				if (contents != null && contents.length == 1)
					return contents[0].getTemplate();
				return null;
			}
		};
		fEditorPasteAction.setActionDefinitionId(IWorkbenchCommandConstants.EDIT_PASTE);
		fTextEditor.setAction(ITextEditorActionConstants.PASTE, fEditorPasteAction);
	}

	/**
	 * Setup a selection listener to monitor the editor
	 */
	private void setupSelectionProvider() {
		ISelectionProvider selectionProvider= fViewer.getSelectionProvider();
		fSelectionChangedListener= new SelectionChangedListener();
		if (selectionProvider instanceof IPostSelectionProvider)
			((IPostSelectionProvider) selectionProvider)
					.addPostSelectionChangedListener(fSelectionChangedListener);
		else
			selectionProvider.addSelectionChangedListener(fSelectionChangedListener);
	}

	/**
	 * Setup the editor site as a drop target.
	 */
	private void setupEditorDropTarget() {
		Control control= fTextEditor.getAdapter(Control.class);
		if (control == null)
			return;

		DropTarget dropTarget= (DropTarget)control.getData(DND.DROP_TARGET_KEY);
		if (dropTarget == null)
			dropTarget= new DropTarget(control, DND.DROP_COPY);

		Transfer[] currentTransfers= dropTarget.getTransfer();
		int currentLength= currentTransfers.length;
		Transfer[] newTransfers= new Transfer[currentLength + 1];
		System.arraycopy(currentTransfers, 0, newTransfers, 0, currentLength);
		newTransfers[currentLength]= TemplatesTransfer.getInstance();
		dropTarget.setTransfer(newTransfers);

		EditorDropTargetListener editorDropTarget= new EditorDropTargetListener();
		dropTarget.addDropListener(editorDropTarget);
	}

	/**
	 * Setup the menu, context menu and toolbar actions.
	 */
	private void setupActions() {
		createActions();
		IActionBars actionBars= getSite().getActionBars();

		actionBars.setGlobalActionHandler(ActionFactory.PASTE.getId(), fPasteAction);
		fPasteAction.setActionDefinitionId(ActionFactory.PASTE.getCommandId());
		fPasteAction.setText(TemplatesMessages.TemplatesPage_paste);
		actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), fCopyAction);
		fCopyAction.setActionDefinitionId(ActionFactory.COPY.getCommandId());
		fCopyAction.setText(TemplatesMessages.TemplatesPage_copy);
		fillToolbar(actionBars);
		fillMenu(actionBars);
	}

	/**
	 * Create all the actions
	 */
	private void createActions() {
		fInsertAction= new Action(TemplatesMessages.TemplatesPage_insert) {
			@Override
			public void run() {
				TemplatePersistenceData[] selectedTemplates= getSelectedTemplates();
				insertTemplate(selectedTemplates[0].getTemplate());
			}
		};
		fInsertAction.setImageDescriptor(TemplatesPageImages
				.getDescriptor(TemplatesPageImages.IMG_ELCL_TEMPLATE_INSERT));
		fInsertAction.setDisabledImageDescriptor(TemplatesPageImages
				.getDescriptor(TemplatesPageImages.IMG_DLCL_TEMPLATE_INSERT));
		fInsertAction.setToolTipText(TemplatesMessages.TemplatesPage_insert_tooltip);

		fAddAction= new Action(TemplatesMessages.TemplatesPage_new) {
			@Override
			public void run() {
				addTemplate();
			}
		};
		fAddAction.setImageDescriptor(TemplatesPageImages
				.getDescriptor(TemplatesPageImages.IMG_ELCL_TEMPLATE_NEW));
		fAddAction.setToolTipText(TemplatesMessages.TemplatesPage_new_tooltip);

		fEditAction= new Action(TemplatesMessages.TemplatesPage_edit) {
			@Override
			public void run() {
				editTemplate();
			}
		};
		fEditAction.setImageDescriptor(TemplatesPageImages
				.getDescriptor(TemplatesPageImages.IMG_ELCL_TEMPLATE_EDIT));
		fEditAction.setDisabledImageDescriptor(TemplatesPageImages
				.getDescriptor(TemplatesPageImages.IMG_DLCL_TEMPLATE_EDIT));
		fEditAction.setToolTipText(TemplatesMessages.TemplatesPage_edit_tooltip);

		fRemoveAction= new Action(TemplatesMessages.TemplatesPage_remove) {
			@Override
			public void run() {
				removeTemplates();
			}
		};
		fRemoveAction.setImageDescriptor(TemplatesPageImages
				.getDescriptor(TemplatesPageImages.IMG_DLCL_TEMPLATE_DELETE));
		fRemoveAction.setImageDescriptor(TemplatesPageImages
				.getDescriptor(TemplatesPageImages.IMG_ELCL_TEMPLATE_DELETE));
		fRemoveAction.setToolTipText(TemplatesMessages.TemplatesPage_remove_tooltip);

		fLinkWithEditorAction= new Action(TemplatesMessages.TemplatesPage_link_to_editor,
				IAction.AS_CHECK_BOX) {
			@Override
			public void run() {
				fPreferenceStore.setValue(LINK_ACTION_PREF_ID, fLinkWithEditorAction.isChecked());
				refresh();
			}
		};
		fLinkWithEditorAction.setImageDescriptor(TemplatesPageImages
				.getDescriptor(TemplatesPageImages.IMG_ELCL_TEMPLATE_LINK));
		fLinkWithEditorAction.setChecked(fPreferenceStore.getBoolean(LINK_ACTION_PREF_ID));
		fLinkWithEditorAction
				.setToolTipText(TemplatesMessages.TemplatesPage_link_to_editor_tooltip);
		fCollapseAllAction= new Action(TemplatesMessages.TemplatesPage_collapse_all) {
			@Override
			public void run() {
				fTreeViewer.collapseAll();
			}
		};
		fCollapseAllAction.setImageDescriptor(TemplatesPageImages
				.getDescriptor(TemplatesPageImages.IMG_ELCL_TEMPLATE_COLLAPSE_ALL));
		fCollapseAllAction.setToolTipText(TemplatesMessages.TemplatesPage_collapse_all_tooltip);

		if (getPreferencePageId() != null) {
			fPreferencePageAction= new Action(TemplatesMessages.TemplatesPage_preference_page) {
				@Override
				public void run() {
					showPreferencePage();
				}
			};
			fPreferencePageAction
					.setToolTipText(TemplatesMessages.TemplatesPage_preference_page_tooltip);
		}

		fPasteAction= new Action() {
			@Override
			public void run() {
				Clipboard clipboard= new Clipboard(getShell().getDisplay());
				try {
					String pattern= ((String)clipboard.getContents(TextTransfer.getInstance()));
					if (pattern != null) {
						final Template template= new Template(createTemplateName(), TemplatesMessages.TemplatesPage_paste_description, getContextTypeId(), pattern.replaceAll("\\$", "\\$\\$"), true); //$NON-NLS-1$//$NON-NLS-2$
						getShell().getDisplay().asyncExec(() -> addTemplate(template));
						return;
					}
					TemplatePersistenceData[] templates= (TemplatePersistenceData[])clipboard.getContents(TemplatesTransfer.getInstance());
					if (templates != null)
						copyTemplates(templates, getContextTypeId());
				} finally {
					clipboard.dispose();
				}

			}
		};

		fCopyAction= new Action() {
			@Override
			public void run() {
				Clipboard clipboard= new Clipboard(getShell().getDisplay());
				try {
					clipboard.setContents(new Object[] { getSelectedTemplates() }, new Transfer[] { TemplatesTransfer.getInstance() });
				} finally {
					clipboard.dispose();
				}
			}
		};
	}

	/**
	 * Inserts the given template into the editor.
	 *
	 * @param template the template
	 */
	private void insertTemplate(Template template) {
		IDocument document= fTextEditor.getDocumentProvider().getDocument(fTextEditor.getEditorInput());
		insertTemplate(template, document);
	}

	/**
	 * Fill the toolbar.
	 *
	 * @param actionBars the action bars
	 */
	private void fillToolbar(IActionBars actionBars) {
		IToolBarManager toolBarManager= actionBars.getToolBarManager();
		toolBarManager.add(fInsertAction);
		toolBarManager.add(fAddAction);
		toolBarManager.add(fEditAction);
		toolBarManager.add(fRemoveAction);

		toolBarManager.add(new Separator());

		toolBarManager.add(fLinkWithEditorAction);
		toolBarManager.add(fCollapseAllAction);
	}

	/**
	 * Fill the view menu.
	 *
	 * @param actionBars the action bars
	 */
	private void fillMenu(IActionBars actionBars) {
		IMenuManager menuManager= actionBars.getMenuManager();

		if (fPreferencePageAction != null) {
			menuManager.add(fPreferencePageAction);
			menuManager.add(new Separator());
		}

		menuManager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
	}

	/**
	 * Fill the context menu items.
	 *
	 * @param manager the menu manager
	 */
	private void fillContextMenu(IMenuManager manager) {
		manager.add(fInsertAction);
		manager.add(new Separator());
		manager.add(fAddAction);
		manager.add(fEditAction);
		manager.add(fRemoveAction);
		manager.add(new Separator());
		manager.add(fCopyAction);
		manager.add(fPasteAction);
		// Other plug-ins can contribute there actions here
		manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
	}

	/**
	 * Create the tree to display templates.
	 *
	 * @param parent the parent composite
	 */
	private void createTemplateTree(Composite parent) {
		Composite treeComposite= new Composite(parent, SWT.NONE);
		GridData data= new GridData(GridData.FILL_BOTH);
		treeComposite.setLayoutData(data);

		TreeColumnLayout columnLayout= new TreeColumnLayout();
		treeComposite.setLayout(columnLayout);
		fTemplatesTree= new Tree(treeComposite, SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI
				| SWT.FULL_SELECTION);
		fTemplatesTree.setHeaderVisible(true);
		fTemplatesTree.setLinesVisible(true);

		PixelConverter pixelConverter= new PixelConverter(fTemplatesTree);

		TreeColumn columnName= new TreeColumn(fTemplatesTree, SWT.NONE);
		columnName.setText(TemplatesMessages.TemplatesPage_column_name);
		int minWidth= fPreferenceStore.getInt(COLUMN_NAME_WIDTH_PREF_ID);
		if (minWidth == 0) {
			minWidth= pixelConverter.convertWidthInCharsToPixels(30);
		}
		columnLayout.setColumnData(columnName, new ColumnPixelData(minWidth, true));
		columnName.addControlListener(new ControlListener() {
			@Override
			public void controlMoved(ControlEvent e) {
			}

			@Override
			public void controlResized(ControlEvent e) {
				int nameWidth= ((TreeColumn) e.getSource()).getWidth();
				fPreferenceStore.setValue(COLUMN_NAME_WIDTH_PREF_ID, nameWidth);
			}
		});

		TreeColumn columnDescription= new TreeColumn(fTemplatesTree, SWT.NONE);
		columnDescription.setText(TemplatesMessages.TemplatesPage_column_description);
		minWidth= fPreferenceStore.getInt(COLUMN_DESCRIPTION_WIDTH_PREF_ID);
		if (minWidth == 0) {
			minWidth= pixelConverter.convertWidthInCharsToPixels(45);
		}
		columnLayout.setColumnData(columnDescription, new ColumnPixelData(minWidth, false));
		columnDescription.addControlListener(new ControlListener() {
			@Override
			public void controlMoved(ControlEvent e) {
			}

			@Override
			public void controlResized(ControlEvent e) {
				int descriptionWidth= ((TreeColumn) e.getSource()).getWidth();
				fPreferenceStore.setValue(COLUMN_DESCRIPTION_WIDTH_PREF_ID, descriptionWidth);
			}
		});

		createTreeViewer(fTemplatesTree);
	}

	/**
	 * Create the tree viewer and setup the providers.
	 *
	 * @param templatesTree the tree used to show the templates
	 */
	private void createTreeViewer(Tree templatesTree) {
		fTreeViewer= new TreeViewer(fTemplatesTree);
		fTreeViewer.setLabelProvider(new TemplateLabelProvider());
		fTreeViewer.setContentProvider(new TemplatesContentProvider());

		fTreeViewer.setComparator(new TemplateViewerComparator());
		fTreeViewer.setInput(getTemplatePreferenceStore());
		fTreeViewer.addDoubleClickListener(e -> {
			updateSelectedItems();
			TemplatePersistenceData[] selectedTemplates = getSelectedTemplates();
			if (selectedTemplates.length > 0)
				insertTemplate(selectedTemplates[0].getTemplate());
		});

		fTreeViewer.addSelectionChangedListener(e -> {
			updateSelectedItems();
			updateButtons();
		});
		fTreeViewer.expandAll();
	}

	/**
	 * Setup the pattern viewer.
	 *
	 * @param parent the parent composite
	 */
	private void createPatternForm(Composite parent) {
		ViewForm viewForm= new ViewForm(parent, SWT.NONE);
		viewForm.setBorderVisible(false);
		CLabel previewLabel= new CLabel(viewForm, SWT.NONE);
		previewLabel.setText(TemplatesMessages.TemplatesPage_preview);
		previewLabel.setImage(TemplatesPageImages.get(TemplatesPageImages.IMG_OBJ_PREVIEW));
		viewForm.setTopLeft(previewLabel);

		fPatternViewer= createPatternViewer(viewForm);
		viewForm.setContent(fPatternViewer.getControl());
		viewForm.addControlListener(new ControlListener() {
			@Override
			public void controlMoved(ControlEvent e) {
			}

			@Override
			public void controlResized(ControlEvent e) {
				int[] weights= fControl.getWeights();
				int sashSize= (int) (weights[0] * 100.0 / (weights[0] + weights[1]));
				fPreferenceStore.setValue(SASH_SIZE_PREF_ID, sashSize);
			}
		});
	}

	/**
	 * Hookup the context menu
	 */
	private void hookContextMenu() {
		MenuManager menuMgr= new MenuManager(POPUP_MENU_ID);
		menuMgr.setRemoveAllWhenShown(true);
		menuMgr.addMenuListener(manager -> fillContextMenu(manager));
		fContextMenu= menuMgr.createContextMenu(fTreeViewer.getControl());
		fTreeViewer.getControl().setMenu(fContextMenu);
		getSite().registerContextMenu(POPUP_MENU_ID, menuMgr, fTreeViewer);
	}

	/**
	 * Check whether the template is valid for the given drop location.
	 *
	 * @param template the template
	 * @param location the drop location
	 * @return <code>true</code> if the template is valid
	 */
	private boolean isTemplateValidAtLocation(Template template, Point location) {
		StyledText textWidget= (StyledText) fTextEditor.getAdapter(Control.class);
		IDocument document= fTextEditor.getDocumentProvider().getDocument(fTextEditor.getEditorInput());
		try {
			if (location.equals(fCachedPosition))
				return fCachedResult;
			fCachedPosition= location;
			int offset= getOffset(document, textWidget, textWidget.toControl(location.x,
					location.y));
			if (fCachedOffset == offset)
				return fCachedResult;
			fCachedOffset= offset;
			if (isValidTemplate(document, template, offset, 0))
				return fCachedResult= true;
		} catch (BadLocationException e) {
		}
		return fCachedResult= false;
	}

	/**
	 * Updates the selected items.
	 */
	private void updateSelectedItems() {
		setSelectedTemplates();
		TemplatePersistenceData[] selectedTemplates= getSelectedTemplates();

		if (selectedTemplates.length == 1)
			updatePatternViewer(selectedTemplates[0].getTemplate());
		else
			updatePatternViewer(null);
	}

	/**
	 * Shows the preference page.
	 */
	private void showPreferencePage() {
		PreferencesUtil.createPreferenceDialogOn(getShell(), getPreferencePageId(), null, null).open();
	}

	/**
	 * Update the state of the buttons
	 */
	private void updateButtons() {
		TemplatePersistenceData[] selectedTemplates= getSelectedTemplates();
		fCopyAction.setEnabled(selectedTemplates.length > 0);
		fInsertAction.setEnabled(selectedTemplates.length == 1);
		fEditAction.setEnabled(selectedTemplates.length == 1);
		fRemoveAction.setEnabled(selectedTemplates.length > 0);
	}

	/**
	 * Set the selected templates
	 */
	private void setSelectedTemplates() {
		IStructuredSelection selection = fTreeViewer.getStructuredSelection();

		Iterator<?> it= selection.iterator();
		TemplatePersistenceData[] data= new TemplatePersistenceData[selection.size()];
		int i= 0;
		while (it.hasNext()) {
			Object o= it.next();
			if (o instanceof TemplatePersistenceData)
				data[i++]= (TemplatePersistenceData) o;
			else {
				fSelectedTemplates= new TemplatePersistenceData[0];
				return;
			}
		}
		fSelectedTemplates= data;
	}

	/**
	 * Returns the currently selected templates
	 *
	 * @return selected templates
	 * @since 3.6 public, before it was private
	 */
	@Override
	public TemplatePersistenceData[] getSelectedTemplates() {
		return fSelectedTemplates;
	}

	/**
	 * Add a template
	 */
	private void addTemplate() {
		String id= getContextTypeId();
		if (id != null) {
			Template template= new Template("", "", id, "", true); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

			addTemplate(template);
		}
	}

	/**
	 * Returns the context type id of the selected template.
	 *
	 * @return the context type id of the selected template or the first from the
	 *         registry if no templates are selected
	 */
	private String getContextTypeId() {
		IStructuredSelection selection = fTreeViewer.getStructuredSelection();
		Object item;

		if (selection.size() == 0)
			return getContextTypeRegistry().contextTypes().next().getId();

		if (selection.size() == 1) {
			item= selection.getFirstElement();
			if (item instanceof TemplatePersistenceData)
				return ((TemplatePersistenceData) item).getTemplate().getContextTypeId();
			return ((TemplateContextType) item).getId();
		}
		Iterator<?> it= selection.iterator();
		String contextId= null;
		while (it.hasNext()) {
			item= it.next();
			if (contextId == null)
				contextId= getContextId(item);
			else if (!contextId.equals(getContextId(item)))
				return getContextTypeRegistry().contextTypes().next()
						.getId();
		}
		return contextId;
	}

	/**
	 * Returns the context id for the given item which is either a template or a context type.
	 *
	 * @param item the item
	 * @return the context type id
	 */
	private String getContextId(Object item) {
		String contextId;
		if (item instanceof TemplatePersistenceData)
			contextId= ((TemplatePersistenceData) item).getTemplate().getContextTypeId();
		else
			contextId= ((TemplateContextType) item).getId();
		return contextId;
	}

	/**
	 * Add a template. The dialog is filled with the values from the given template.
	 *
	 * @param template the template
	 */
	private void addTemplate(Template template) {
		Template newTemplate;
		newTemplate= editTemplate(template, false, true);
		if (newTemplate != null) {
			TemplatePersistenceData data= new TemplatePersistenceData(newTemplate, true);
			getTemplateStore().add(data);
			saveTemplateStore();
			refresh();
			fTreeViewer.setSelection(new StructuredSelection(data), true);
		}
	}

	/**
	 * Save the template store
	 */
	private void saveTemplateStore() {
		try {
			getTemplateStore().save();
		} catch (IOException e) {
			TextEditorPlugin.getDefault().getLog().log(new Status(IStatus.ERROR, TextEditorPlugin.PLUGIN_ID,
					TemplatesMessages.TemplatesPage_save_error_message, e));
			MessageDialog.openError(getShell(),
					TemplatesMessages.TemplatesPage_save_error_message, e.getMessage());
		}
	}

	/**
	 * Edits the selected template.
	 */
	private void editTemplate() {
		TemplatePersistenceData selectedTemplate= getSelectedTemplates()[0];
		Template oldTemplate= selectedTemplate.getTemplate();
		Template newTemplate= editTemplate(new Template(oldTemplate), true, true);
		if (newTemplate != null) {
			if (!newTemplate.getName().equals(oldTemplate.getName())
					&& MessageDialog.openQuestion(getShell(),
							TemplatesMessages.TemplatesPage_question_create_new_title,
							TemplatesMessages.TemplatesPage_question_create_new_message)) {
				TemplatePersistenceData templateData= new TemplatePersistenceData(newTemplate,
						true);
				getTemplateStore().add(templateData);
				refresh();
				fTreeViewer.setSelection(new StructuredSelection(templateData), true);
			} else {
				selectedTemplate.setTemplate(newTemplate);
				updatePatternViewer(newTemplate);
			}
		}
		saveTemplateStore();
	}

	/**
	 * Moves the selected template from one context to another.
	 *
	 * @param templates an array of template data
	 * @param contextId the contextId
	 *
	 */
	private void moveTemplates(TemplatePersistenceData[] templates, String contextId) {
		for (int i= 0; i < templates.length; i++) {
			Template t= templates[i].getTemplate();
			templates[i].setTemplate(new Template(t.getName(), t.getDescription(), contextId, t
					.getPattern(), t.isAutoInsertable()));
		}
		saveTemplateStore();
		fTreeViewer.setSelection(new StructuredSelection(templates), true);
	}

	/**
	 * Copy the selected templates to another context
	 *
	 * @param templates an array of template data
	 * @param contextId the context id
	 *
	 */
	private void copyTemplates(TemplatePersistenceData[] templates, String contextId) {
		TemplatePersistenceData[] newTemplates= new TemplatePersistenceData[templates.length];
		for (int i= 0; i < templates.length; i++) {
			Template t= templates[i].getTemplate();
			newTemplates[i]= new TemplatePersistenceData(new Template(t.getName(), t
					.getDescription(), contextId, t.getPattern(), t.isAutoInsertable()), true);
			getTemplateStore().add(newTemplates[i]);
		}
		saveTemplateStore();
		refresh();
		fTreeViewer.setSelection(new StructuredSelection(newTemplates), true);
	}

	/**
	 * Remove one or more selected templates
	 */
	private void removeTemplates() {
		String title;
		TemplatePersistenceData[] selectedTemplates= getSelectedTemplates();
		if (selectedTemplates.length == 1)
			title= TemplatesMessages.TemplatesPage_remove_title_single;
		else
			title= TemplatesMessages.TemplatesPage_remove_title_multi;
		String message;
		if (selectedTemplates.length == 1)
			message= TemplatesMessages.TemplatesPage_remove_message_single;
		else
			message= NLSUtility.format(TemplatesMessages.TemplatesPage_remove_message_multi,
					new Object[] { Integer.valueOf(selectedTemplates.length) });
		if (!MessageDialog.openQuestion(getShell(), title, message))
			return;
		for (int i= 0; i < selectedTemplates.length; i++) {
			getTemplateStore().delete(selectedTemplates[i]);
		}
		saveTemplateStore();
		fTreeViewer.setSelection(new StructuredSelection(new Object[] {}), true);
	}

	/**
	 * Initializes drag and drop the template items
	 */
	private void initializeDND() {
		DragSourceAdapter dragListener= new DragSourceAdapter() {
			@Override
			public void dragStart(DragSourceEvent event) {
				if (getSelectedTemplates().length == 0) {
					event.doit= false;
				}
			}

			@Override
			public void dragSetData(DragSourceEvent event) {
				if (TemplatesTransfer.getInstance().isSupportedType(event.dataType)) {
					event.data= getSelectedTemplates();
				}
			}
		};
		fTreeViewer.addDragSupport(DND.DROP_COPY | DND.DROP_MOVE, new Transfer[] { TemplatesTransfer
				.getInstance() }, dragListener);
		DropTargetAdapter dropListener= new DropTargetAdapter() {
			Transfer textTransfer= TextTransfer.getInstance();
			Transfer templateTransfer= TemplatesTransfer.getInstance();

			@Override
			public void dragEnter(DropTargetEvent event) {
				if (event.detail == DND.DROP_DEFAULT)
					event.detail= DND.DROP_COPY;
			}

			@Override
			public void dragOperationChanged(DropTargetEvent event) {
				if (event.detail == DND.DROP_DEFAULT)
					event.detail= DND.DROP_COPY;
			}

			@Override
			public void dragOver(DropTargetEvent event) {
				event.feedback |= DND.FEEDBACK_SCROLL;
				if (event.item == null) {
					event.detail= DND.DROP_NONE;
					return;
				}
				int index= 0;
				boolean isTemplateTransfer= false;
				while (index < event.dataTypes.length) {
					if (textTransfer.isSupportedType(event.dataTypes[index])) {
						break;
					}
					if (templateTransfer.isSupportedType(event.dataTypes[index])) {
						isTemplateTransfer= true;
						break;
					}
					index++;
				}
				if (index < event.dataTypes.length) {
					event.currentDataType= event.dataTypes[index];
					if (event.detail == DND.DROP_DEFAULT || !isTemplateTransfer)
						event.detail= DND.DROP_COPY;
					return;
				}
			}

			@Override
			public void drop(DropTargetEvent event) {
				if (event.item == null)
					return;
				Object object= ((TreeItem) event.item).getData();
				final String contextId;
				if (object instanceof TemplateContextType)
					contextId= ((TemplateContextType) object).getId();
				else
					contextId= ((TemplatePersistenceData) object).getTemplate().getContextTypeId();
				if (textTransfer.isSupportedType(event.currentDataType)) {
					String text= ((String) event.data).replaceAll("\\$", "\\$\\$"); //$NON-NLS-1$ //$NON-NLS-2$
					final Template template= new Template(createTemplateName(),
							TemplatesMessages.TemplatesPage_paste_description, contextId, text,
							true);
					getShell().getDisplay().asyncExec(() -> addTemplate(template));
					return;
				}
				if (templateTransfer.isSupportedType(event.currentDataType)) {
					final TemplatePersistenceData[] templates= (TemplatePersistenceData[]) event.data;
					final int dropType= event.detail;
					getShell().getDisplay().asyncExec(() -> {
						if (dropType == DND.DROP_COPY)
							copyTemplates(templates, contextId);
						else
							moveTemplates(templates, contextId);
					});
				}
			}
		};
		Transfer[] transfers= new Transfer[] { TextTransfer.getInstance(),
				TemplatesTransfer.getInstance() };
		fTreeViewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE, transfers, dropListener);
	}

	/**
	 * Create a template name.
	 *
	 * @return the created template name
	 */
	private String createTemplateName() {
		for (int i= 1; i < Integer.MAX_VALUE; i++) {
			String name= TemplatesMessages.TemplatesPage_snippet + i;
			if (getTemplateStore().findTemplate(name) == null)
				return name;
		}
		return null;
	}

	/**
	 * Stores the collapse state of a context node.
	 */
	private void storeCollapseState() {
		TreeItem[] items= fTreeViewer.getTree().getItems();
		for (int i= 0; i < items.length; i++) {
			fPreferenceStore.setValue(CONTEXT_COLLAPSE_PREF_ID
					+ ((TemplateContextType) items[i].getData()).getId(), !items[i].getExpanded());
		}
	}

	/**
	 * Refreshes the template tree contents.
	 */
	private void refresh() {
		storeCollapseState();
		fTreeViewer.getTree().setRedraw(false);
		try {
			fTreeViewer.refresh();
			TreeItem[] items= fTreeViewer.getTree().getItems();
			for (int i= 0; i < items.length; i++) {
				boolean isExpanded= !fPreferenceStore.getBoolean(CONTEXT_COLLAPSE_PREF_ID
						+ ((TemplateContextType) items[i].getData()).getId());
				if (isExpanded)
					fTreeViewer.expandToLevel(items[i].getData(), AbstractTreeViewer.ALL_LEVELS);
				else
					fTreeViewer.collapseToLevel(items[i].getData(), AbstractTreeViewer.ALL_LEVELS);
			}
		} finally {
			fTreeViewer.getTree().setRedraw(true);
		}
	}

	/**
	 * Returns the document relative offset from the text widget relative point
	 *
	 * @param document the document
	 * @param textWidget the text widget
	 * @param point the point for which to get the offset
	 * @return the offset
	 * @throws BadLocationException if the document is accessed with an invalid line
	 */
	private int getOffset(IDocument document, StyledText textWidget, Point point)
			throws BadLocationException {
		int widgetCaret= fViewer.getTextWidget().getCaretOffset();
		if (fViewer instanceof ITextViewerExtension5) {
			ITextViewerExtension5 ext= (ITextViewerExtension5) fViewer;
			int widgetOffset = textWidget.getOffsetAtPoint(point);
			int offset = widgetOffset != -1 ? ext.widgetOffset2ModelOffset(textWidget.getOffsetAtPoint(point)) : -1;
			if (offset == -1) {
				int docLineIndex= ext.widgetLine2ModelLine(textWidget.getLineIndex(point.y));
				String lineDelimiter= document.getLineDelimiter(docLineIndex);
				int delimLength= lineDelimiter == null ? 0 : lineDelimiter.length();
				return document.getLineOffset(docLineIndex) + document.getLineLength(docLineIndex)
						- delimLength;
			}
			return offset;
		}
		IRegion visible= fViewer.getVisibleRegion();
		return widgetCaret + visible.getOffset();
	}
}
