blob: 9c2d0878c8fbe7e8dc77529bb1414cb1abce9376 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2018 NumberFour AG and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* NumberFour AG - initial API and Implementation (Jens von Pilgrim)
*******************************************************************************/
package org.eclipse.dltk.ui.wizards;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.IProjectFragment;
import org.eclipse.dltk.core.IScriptFolder;
import org.eclipse.dltk.core.IScriptModel;
import org.eclipse.dltk.core.IScriptProject;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.internal.ui.dialogs.TextFieldNavigationHandler;
import org.eclipse.dltk.internal.ui.wizards.NewWizardMessages;
import org.eclipse.dltk.internal.ui.wizards.TypedElementSelectionValidator;
import org.eclipse.dltk.internal.ui.wizards.dialogfields.DialogField;
import org.eclipse.dltk.internal.ui.wizards.dialogfields.IDialogFieldListener;
import org.eclipse.dltk.internal.ui.wizards.dialogfields.IStringButtonAdapter;
import org.eclipse.dltk.internal.ui.wizards.dialogfields.LayoutUtil;
import org.eclipse.dltk.internal.ui.wizards.dialogfields.StringButtonDialogField;
import org.eclipse.dltk.ui.DLTKUIPlugin;
import org.eclipse.dltk.ui.ModelElementLabelProvider;
import org.eclipse.dltk.ui.dialogs.StatusInfo;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.dialogs.ElementListSelectionDialog;
import org.eclipse.ui.dialogs.ISelectionStatusValidator;
/**
* Wizard page that acts as a base class for wizard pages that create new Script
* elements, supporting the notion of packages (i.e. namespaces). Source folder
* selection is restricted to the root source folders. Its sub-folders are
* interpreted as packages and can be selected accordingly. By enabling
* <code>{@link #setAutoCreateMissingPackages(boolean) autoCreateMissingPackages}</code>
* , the wizard does also create missing packages automatically when
* {@link #createFile(IProgressMonitor) creating} the file.
* <p>
* This page behaves quite similar to its super class (cf. Liskov substitution
* principle). That is, clients can use the {@link NewSourceModuleInPackagePage}
* just like a {@link NewSourceModulePage}, by using
* <code>{@link #setScriptFolder(IScriptFolder, boolean) set}/{@link #getScriptFolder() get}ScriptFolder(..)</code>
* . The separation of source folder and contained packages is transparently
* handled. Client can also access the source folder and package path
* separately, though (cf.
* <code>{@link #setSource(IProjectFragment, boolean) set}/{@link #getSource() get}Source(..)</code>
* and
* <code>{@link #setPackage(IPath, boolean) set}/{@link #getPackage() get}Package(..))</code>
* .
* </p>
*/
public abstract class NewSourceModuleInPackagePage extends NewSourceModulePage {
/** Id of the package field */
protected static final String PACKAGE = "NewPackagedSourceModulePage.package"; //$NON-NLS-1$
/**
* Status of last validation of the package field
*/
protected IStatus packageStatus;
private boolean sourceIsModifiable = true;
private boolean autoCreateMissingPackages = false;
private StringButtonDialogField fPackageDialogField;
/**
* Package in terms of a path relative to the source folder (i.e.
* container).
*/
private IPath currentPackagePath = null;
private class PackageFieldAdapter
implements IStringButtonAdapter, IDialogFieldListener {
@Override
public void dialogFieldChanged(DialogField field) {
packageDialogFieldChanged();
}
@Override
public void changeControlPressed(DialogField field) {
packageChangeControlPressed();
}
}
public NewSourceModuleInPackagePage() {
PackageFieldAdapter packageFieldAdapter = new PackageFieldAdapter();
fPackageDialogField = new StringButtonDialogField(packageFieldAdapter);
fPackageDialogField.setDialogFieldListener(packageFieldAdapter);
fPackageDialogField.setButtonLabel(
NewWizardMessages.NewSourceModuleInPackagePage_package_button);
fPackageDialogField.setLabelText(getPackageLabel());
packageStatus = new StatusInfo();
}
/**
* Returns whether missing packages are automatically created on finish,
* i.e. in {@link #createFile(org.eclipse.core.runtime.IProgressMonitor)}.
* Default value (after constrution) is false.
*/
public boolean isAutoCreateMissingPackages() {
return autoCreateMissingPackages;
}
/**
* Sets whether missing packages are to be automatically created on finish
* or not. This also influences verification of page, as it must not be
* finished with non-exisiting packages in case this is set to false.
* Default value (after constrution) is false.
*/
public void setAutoCreateMissingPackages(
boolean autoCreateMissingPackages) {
this.autoCreateMissingPackages = autoCreateMissingPackages;
}
public void setSource(IProjectFragment src, boolean canBeModified) {
sourceIsModifiable = canBeModified;
if (src == null) {
super.setScriptFolder(null, sourceIsModifiable);
} else {
super.setScriptFolder(src.getScriptFolder(""), sourceIsModifiable);
}
}
public IProjectFragment getSource() {
return getProjectFragment();
}
public void setPackage(IPath srcRelativePath, boolean canBeModified) {
currentPackagePath = srcRelativePath;
if (currentPackagePath == null) {
fPackageDialogField.setText("");
} else {
String str = currentPackagePath.toString(); // $NON-NLS-1$
fPackageDialogField.setText(str);
}
fPackageDialogField.setEnabled(canBeModified);
}
public IPath getPackage() {
return currentPackagePath;
}
@Override
public void setScriptFolder(IScriptFolder root, boolean canBeModified) {
IProjectFragment src = null;
IPath packPath = null;
if (root != null) {
src = (IProjectFragment) root
.getAncestor(IModelElement.PROJECT_FRAGMENT);
if (src != null) {
IPath srcPath = src.getPath();
packPath = root.getPath().makeRelativeTo(srcPath);
}
}
setSource(src, canBeModified);
setPackage(packPath, canBeModified);
}
@Override
public IScriptFolder getScriptFolder() {
IProjectFragment src = getSource();
if (src != null) {
if (currentPackagePath == null) {
return src.getScriptFolder("");
}
return src.getScriptFolder(currentPackagePath);
}
return null;
}
protected String getPackageLabel() {
return NewWizardMessages.NewSourceModuleInPackagePage_package_label;
}
/**
* Returns the content of the package input field.
*
* @return the content of the package input field
*/
public String getPackageText() {
return fPackageDialogField.getText();
}
/**
* Sets the content of the package input field to the given value.
*
* @param str
* the new package input field text
* @param canBeModified
* if <code>true</code> the package input field can
* be modified; otherwise it is read-only.
*/
public void setPackageText(String str, boolean canBeModified) {
fPackageDialogField.setText(str);
fPackageDialogField.setEnabled(canBeModified);
}
@Override
void containerChangeControlPressed(DialogField field) {
IScriptFolder root = chooseContainer();
if (root != null) {
setSource(
(IProjectFragment) root
.getAncestor(IModelElement.PROJECT_FRAGMENT),
sourceIsModifiable);
}
}
/**
* Opens a selection dialog that allows to select a source container.
*
* @return returns the selected package fragment root or <code>null</code>
* if the dialog has been canceled. The caller typically sets the
* result to the container input field.
* <p>
* Clients can override this method if they want to offer a
* different dialog.
* </p>
*
*
*/
@Override
protected IScriptFolder chooseContainer() {
IModelElement initElement = getProjectFragment();
Class<?>[] shownTypes = new Class[] { IScriptModel.class,
IScriptProject.class, IProjectFragment.class };
ViewerFilter filter = new ContainerViewerFilter(shownTypes);
Class<?>[] acceptedTypes = new Class[] { IProjectFragment.class };
ISelectionStatusValidator validator = new TypedElementSelectionValidator(
acceptedTypes, false);
IScriptFolder scriptFolder = doChooseContainer(initElement, filter,
validator);
return scriptFolder;
}
/**
* Calls {@link NewContainerWizardPage#containerChanged()} and additionally
* checks if currently selected source folder is a Script project's source
* folder.
*/
@Override
protected IStatus containerChanged() {
IStatus status = super.containerChanged();
if (status.isOK()) {
try {
if (getSource().getKind() == IProjectFragment.K_SOURCE
&& !getSource().isExternal()) {
return status;
}
} catch (ModelException e) {
DLTKUIPlugin.log(e);
}
StatusInfo statusInfo = new StatusInfo();
statusInfo.setError(
NewWizardMessages.NewSourceModuleInPackagePage_error_ContainerIsNoSourceFolder);
status = statusInfo;
}
return status;
}
private void packageChangeControlPressed() {
IScriptFolder packageFolder = choosePackage();
if (packageFolder == null) {
return;
}
IProjectFragment projectFragment = (IProjectFragment) packageFolder
.getAncestor(IModelElement.PROJECT_FRAGMENT);
if (projectFragment != null) {
IPath path = packageFolder.getPath()
.makeRelativeTo(projectFragment.getPath());
setPackage(path, true);
} else {
DLTKUIPlugin.logErrorMessage(
"Illegal state, chosen package is not contained in a project fragment"); // $NON-NLS-N$
}
}
/**
* Opens a selection dialog that allows to select a package.
*
* @return returns the selected package or <code>null</code> if the dialog
* has been canceled. The caller typically sets the result to the
* package input field.
* <p>
* Clients can override this method if they want to offer a
* different dialog.
* </p>
*
*
*/
protected IScriptFolder choosePackage() {
IScriptFolder[] packages = getAllPackages();
ILabelProvider labelProvider = new ModelElementLabelProvider(
ModelElementLabelProvider.SHOW_DEFAULT);
ElementListSelectionDialog dialog = new ElementListSelectionDialog(
getShell(), labelProvider);
dialog.setIgnoreCase(false);
dialog.setTitle(
NewWizardMessages.NewSourceModuleInPackagePage_ChoosePackageDialog_title);
dialog.setMessage(
NewWizardMessages.NewSourceModuleInPackagePage_ChoosePackageDialog_description);
dialog.setEmptyListMessage(
NewWizardMessages.NewSourceModuleInPackagePage_ChoosePackageDialog_empty);
dialog.setElements(packages);
dialog.setHelpAvailable(false);
IScriptFolder packFolder = getScriptFolder();
if (packFolder != null) {
dialog.setInitialSelections(new Object[] { packFolder });
}
if (dialog.open() == Window.OK) {
return (IScriptFolder) dialog.getFirstResult();
}
return null;
}
/**
* Returns all packages in the currently selected source folder to be shown
* in the package selection dialog. The default implementation simply
* returns all sub-folders of the current source folder, including the
* default package (i.e. the source folder itself).
* <p>
* Subclasses may override in case other packages are to be displayed. Note
* that sorting of packages is done by dialog when using the default
* {@link #choosePackage()} implementation.
* </p>
*/
protected IScriptFolder[] getAllPackages() {
Collection<IScriptFolder> packages = new ArrayList<>();
IProjectFragment sourceFolder = getProjectFragment();
if (sourceFolder != null) {
try {
for (IModelElement e : sourceFolder.getChildren()) {
if (e instanceof IScriptFolder) {
packages.add((IScriptFolder) e);
}
}
} catch (ModelException e) {
DLTKUIPlugin.log(e);
}
}
return packages.toArray(new IScriptFolder[packages.size()]);
}
private void packageDialogFieldChanged() {
packageStatus = packageChanged();
// tell all others
handleFieldChanged(PACKAGE);
}
/*
* Verifies the input for the package field.
*/
protected IStatus packageChanged() {
StatusInfo status = new StatusInfo();
String packName = getPackageText();
if (packName.length() == 0) {
status.setError(
NewWizardMessages.NewPackageWizardPage_error_EnterName);
return status;
}
IProjectFragment root = getProjectFragment();
if (root != null && root.getScriptProject().exists()) {
IScriptFolder pack = root.getScriptFolder(packName);
IPath srcPath = root.getPath();
currentPackagePath = pack.getPath().makeRelativeTo(srcPath);
try {
// IPath rootPath = root.getPath();
if (!pack.exists()) {
URI location = pack.getResource().getLocationURI();
if (location != null) {
IFileStore store = EFS.getStore(location);
if (store.fetchInfo().exists()) {
status.setError(
NewWizardMessages.NewPackageWizardPage_error_PackageExistsDifferentCase);
}
}
if (!status.isError() && !autoCreateMissingPackages) {
status.setError(
NewWizardMessages.NewSourceModuleInPackagePage_error_PackageDoesNotExist);
}
}
} catch (CoreException e) {
DLTKUIPlugin.log(e);
}
}
return status;
}
/**
* Hook method that gets called when a field on this page has changed. For
* this page the method gets called when the source folder field changes.
* <p>
* Every sub type is responsible to call this method when a field on its
* page has changed. Subtypes override (extend) the method to add
* verification when a own field has a dependency to an other field. For
* example the class name input must be verified again when the package
* field changes (check for duplicated class names).
*
* @param fieldName
* The name of the field that has changed (field id).
* For the source folder the field id is
* {@link NewContainerWizardPage#CONTAINER} , for the
* package it's {@link #PACKAGE}, and for the file name
* {@link NewSourceModulePage#FILE}
*/
@Override
protected void handleFieldChanged(String fieldName) {
super.handleFieldChanged(fieldName);
if (PACKAGE.equals(fieldName)) {
// super classes have to update script folder accordingly
super.handleFieldChanged(CONTAINER);
}
if (CONTAINER.equals(fieldName)) {
packageStatus = packageChanged();
}
// do status line update
updateStatus(new IStatus[] { containerStatus, packageStatus,
sourceModuleStatus });
}
/**
* Calls {@link NewSourceModulePage#createFile(IProgressMonitor)} and
* automatically creates missing packages, if
* {@link #isAutoCreateMissingPackages()} returns true.
*/
@Override
public ISourceModule createFile(IProgressMonitor monitor)
throws CoreException {
if (autoCreateMissingPackages) {
String packageName = getPackage().toString();
getProjectFragment().createScriptFolder(packageName, true, monitor);
}
return super.createFile(monitor);
}
/**
* Creates the necessary controls (label, text field and browse button) to
* edit the source folder location and the package folder. The method
* expects that the parent composite uses a <code>GridLayout</code> as its
* layout manager and that the grid layout has at least 3 columns.
*
* @param parent
* the parent composite
* @param nColumns
* the number of columns to span. This number must be
* greater or equal three
*/
@Override
protected void createContainerControls(Composite parent, int nColumns) {
super.createContainerControls(parent, nColumns);
createPackageControls(parent, nColumns);
}
/**
* Creates the controls for the package name field. Expects a
* <code>GridLayout</code> with at least 3 columns. This method must only be
* called if packages are supported.
*
* @param composite
* the parent composite
* @param nColumns
* number of columns to span
*/
protected void createPackageControls(Composite composite, int nColumns) {
fPackageDialogField.doFillIntoGrid(composite, nColumns);
Text text = fPackageDialogField.getTextControl(null);
LayoutUtil.setWidthHint(text, getMaxFieldWidth());
LayoutUtil.setHorizontalGrabbing(text);
DialogField.createEmptySpace(composite);
TextFieldNavigationHandler.install(text);
}
}