/*******************************************************************************
* Copyright (c) 2000, 2009 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Jakub Jurkiewicz <jakub.jurkiewicz@gmail.com> - Fix for Bug 174737
* [IDE] New Plug-in Project wizard status handling is inconsistent
* Oakland Software Incorporated (Francis Upton) <francisu@ieee.org>
* Bug 224997 [Workbench] Impossible to copy project
*******************************************************************************/
package com.aptana.ruby.ui.wizards;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.DirectoryDialog;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.ide.IIDEHelpContextIds;
import org.eclipse.ui.internal.ide.dialogs.IDEResourceInfoUtils;
import com.aptana.core.CoreStrings;
import com.aptana.core.projects.templates.IProjectTemplate;
import com.aptana.core.util.StringUtil;
import com.aptana.projects.wizards.AbstractNewProjectWizard;
import com.aptana.projects.wizards.IStepIndicatorWizardPage;
import com.aptana.projects.wizards.IWizardProjectCreationPage;
import com.aptana.ui.util.SWTUtils;
import com.aptana.ui.util.UIUtils;
import com.aptana.ui.widgets.SelectedTemplateComposite;
import com.aptana.ui.widgets.StepIndicatorComposite;
/**
* TODO Extract common code between this and our Web project wizard!
*/
@SuppressWarnings("restriction")
public class WizardNewRubyProjectCreationPage extends WizardPage implements IWizardProjectCreationPage,
IStepIndicatorWizardPage
{
// initial value stores
private String initialProjectFieldValue;
// widgets
private Text projectNameField;
private StyledText locationPathField;
private Button browseButton;
private Button gitCloneGenerate;
private StyledText gitLocation;
private String lastGitDefault;
private Button gitBrowseButton;
private Listener nameModifyListener = new Listener()
{
public void handleEvent(Event e)
{
updateProjectName(getProjectNameFieldValue());
setPageComplete(validatePage());
}
};
private String lastLocationDefault;
protected Button noGenerator;
private Group projectGenerationGroup;
private StackLayout projectGenerationStackLayout;
private Composite projectGenerationControls;
private Label warningLabel;
// Initial Project template
private IProjectTemplate projectTemplate = null;
// Used for step indicator composite
protected StepIndicatorComposite stepIndicatorComposite;
protected String[] stepNames;
// constants
private static final int SIZING_TEXT_FIELD_WIDTH = 250;
private static final String SAVED_LOCATION_ATTR = "OUTSIDE_LOCATION"; //$NON-NLS-1$
private static final String EMPTY_STRING = StringUtil.EMPTY;
/**
* Creates a new project creation wizard page.
*
* @param pageName
* the name of this page
*/
public WizardNewRubyProjectCreationPage(String pageName, IProjectTemplate projectTemplate)
{
super(pageName);
setPageComplete(false);
this.projectTemplate = projectTemplate;
}
/*
* (non-Javadoc)
* @see org.eclipse.jface.wizard.WizardPage#canFlipToNextPage()
*/
@Override
public boolean canFlipToNextPage()
{
return super.canFlipToNextPage() && noGenerator.getSelection();
}
/**
* (non-Javadoc) Method declared on IDialogPage.
*/
public void createControl(Composite parent)
{
Composite pageComposite = new Composite(parent, SWT.NONE);
GridLayout pageLayout = GridLayoutFactory.fillDefaults().spacing(0, 5).create();
pageComposite.setLayout(pageLayout);
pageComposite.setLayoutData(GridDataFactory.fillDefaults().create());
stepIndicatorComposite = new StepIndicatorComposite(pageComposite, stepNames);
stepIndicatorComposite.setSelection(getStepName());
initializeDialogUnits(parent);
PlatformUI.getWorkbench().getHelpSystem().setHelp(pageComposite, IIDEHelpContextIds.NEW_PROJECT_WIZARD_PAGE);
createProjectNameGroup(pageComposite);
// create Location section
createDestinationLocationArea(pageComposite);
// Create the project template section (if required)
createProjectTemplateSection(pageComposite);
// create warning section
createWarningArea(pageComposite);
// Add the generate app section
createGenerateGroup(pageComposite);
// Scale the button based on the rest of the dialog
setButtonLayoutData(browseButton);
updateProjectName(getProjectNameFieldValue());
setPageComplete(validatePage());
// Show description on opening
setErrorMessage(null);
setMessage(null);
setControl(pageComposite);
Dialog.applyDialogFont(pageComposite);
}
protected void createProjectTemplateSection(Composite parent)
{
if (projectTemplate != null)
{
new SelectedTemplateComposite(parent, projectTemplate);
}
return;
}
/**
* Create the area for destination location of project.
*
* @param composite
* @param defaultEnabled
*/
private void createDestinationLocationArea(Composite composite)
{
// project specification group
Composite projectGroup = new Composite(composite, SWT.NONE);
GridLayout layout = new GridLayout();
layout.numColumns = 4;
projectGroup.setLayout(layout);
projectGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
// location label
Label locationLabel = new Label(projectGroup, SWT.NONE);
locationLabel.setText(Messages.ProjectLocationSelectionDialog_locationLabel);
// project location entry field
locationPathField = new SpecialText(projectGroup, SWT.BORDER | SWT.SINGLE)
{
@Override
protected boolean matchesLastDefault()
{
return locationMatchesLastDefault();
}
protected boolean isDefault()
{
String defaultLocation = Platform.getLocation().append(getProjectNameFieldValue()).toOSString();
return Path.fromOSString(locationPathField.getText()).toOSString().equals(defaultLocation);
};
};
GridData data = new GridData(GridData.FILL_HORIZONTAL);
data.widthHint = SIZING_TEXT_FIELD_WIDTH;
data.horizontalSpan = 2;
locationPathField.setLayoutData(data);
// browse button
browseButton = new Button(projectGroup, SWT.PUSH);
browseButton.setText(StringUtil.ellipsify(CoreStrings.BROWSE));
browseButton.addSelectionListener(new SelectionAdapter()
{
public void widgetSelected(SelectionEvent event)
{
handleLocationBrowseButtonPressed();
}
});
if (initialProjectFieldValue == null)
{
locationPathField.setText(EMPTY_STRING);
}
else
{
locationPathField.setText(initialProjectFieldValue);
}
locationPathField.addModifyListener(new ModifyListener()
{
/*
* (non-Javadoc)
* @see org.eclipse.swt.events.ModifyListener#modifyText(org.eclipse.swt.events.ModifyEvent)
*/
public void modifyText(ModifyEvent e)
{
reportError(checkValidLocation(), false);
}
});
}
private void createWarningArea(Composite composite)
{
final Font font = new Font(composite.getDisplay(), SWTUtils.italicizedFont(getFont()));
warningLabel = new Label(composite, SWT.WRAP);
warningLabel.setLayoutData(GridDataFactory.fillDefaults().align(SWT.FILL, SWT.BEGINNING).grab(true, false)
.create());
warningLabel.setFont(font);
warningLabel.setForeground(UIUtils.getDisplay().getSystemColor(SWT.COLOR_RED));
composite.addDisposeListener(new DisposeListener()
{
public void widgetDisposed(DisposeEvent e)
{
if (font != null && !font.isDisposed())
{
font.dispose();
}
}
});
}
/**
* Return the path on the location field.
*
* @return String
*/
private String getPathFromLocationField()
{
URI fieldURI;
try
{
fieldURI = new URI(getLocationText());
}
catch (URISyntaxException e)
{
return locationPathField.getText();
}
return fieldURI.getPath();
}
protected String getLocationText()
{
return locationPathField.getText();
}
/**
* Open an appropriate directory browser
*/
protected void handleLocationBrowseButtonPressed()
{
String selectedDirectory = null;
String dirName = getPathFromLocationField();
if (!dirName.equals(EMPTY_STRING))
{
IFileInfo info = IDEResourceInfoUtils.getFileInfo(dirName);
if (info == null || !(info.exists()))
{
dirName = EMPTY_STRING;
}
}
else
{
String value = getDialogSettings().get(SAVED_LOCATION_ATTR);
if (value != null)
{
dirName = value;
}
}
DirectoryDialog dialog = new DirectoryDialog(locationPathField.getShell(), SWT.SHEET);
dialog.setMessage(EMPTY_STRING);
dialog.setFilterPath(dirName);
selectedDirectory = dialog.open();
if (selectedDirectory != null)
{
locationPathField.setText(selectedDirectory);
getDialogSettings().put(SAVED_LOCATION_ATTR, selectedDirectory);
}
}
/**
* Open an appropriate directory browser
*/
protected void handleGitLocationBrowseButtonPressed()
{
String dirName = gitLocation.getText().trim();
if (!dirName.equals(EMPTY_STRING))
{
IFileInfo info = IDEResourceInfoUtils.getFileInfo(dirName);
if (info == null || !(info.exists()))
{
dirName = EMPTY_STRING;
}
}
DirectoryDialog dialog = new DirectoryDialog(gitLocation.getShell(), SWT.SHEET);
dialog.setMessage(EMPTY_STRING);
dialog.setFilterPath(dirName);
String selectedDirectory = dialog.open();
if (selectedDirectory != null)
{
gitLocation.setText(selectedDirectory);
selectGitCloneGeneration();
}
}
protected String checkValidLocation()
{
String locationFieldContents = locationPathField.getText();
if (locationFieldContents.length() == 0)
{
return Messages.WizardNewProjectCreationPage_projectLocationEmpty;
}
return null;
}
protected String checkValidGitLocation()
{
if (!isCloneFromGit())
return null;
String locationFieldContents = gitLocation.getText();
if (locationFieldContents.length() == 0)
{
return Messages.WizardNewProjectCreationPage_gitLocationEmpty;
}
return null;
}
protected void reportError(String errorMessage, boolean infoOnly)
{
if (infoOnly)
{
setMessage(errorMessage, IStatus.INFO);
setErrorMessage(null);
}
else
{
setErrorMessage(errorMessage);
}
boolean valid = errorMessage == null;
if (valid)
{
valid = validatePage();
}
setPageComplete(valid);
}
/**
* Creates the project name specification controls.
*
* @param parent
* the parent composite
*/
private final void createProjectNameGroup(Composite parent)
{
// project specification group
Composite projectGroup = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout();
layout.numColumns = 2;
projectGroup.setLayout(layout);
projectGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
// new project label
Label projectLabel = new Label(projectGroup, SWT.NONE);
projectLabel.setText(Messages.WizardNewProjectCreationPage_nameLabel);
projectLabel.setFont(parent.getFont());
// new project name entry field
projectNameField = new Text(projectGroup, SWT.BORDER);
GridData data = new GridData(GridData.FILL_HORIZONTAL);
data.widthHint = SIZING_TEXT_FIELD_WIDTH;
projectNameField.setLayoutData(data);
projectNameField.setFont(parent.getFont());
// Set the initial value first before listener
// to avoid handling an event during the creation.
if (initialProjectFieldValue != null)
{
projectNameField.setText(initialProjectFieldValue);
}
projectNameField.addListener(SWT.Modify, nameModifyListener);
}
/**
* Creates the project generation controls.
*
* @param parent
* the parent composite
*/
protected Composite createGenerateGroup(Composite parent)
{
// project generation group
projectGenerationGroup = new Group(parent, SWT.NONE);
projectGenerationGroup.setText(Messages.WizardNewProjectCreationPage_GenerateAppGroupLabel);
projectGenerationStackLayout = new StackLayout();
projectGenerationGroup.setLayout(projectGenerationStackLayout);
projectGenerationGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
projectGenerationControls = new Composite(projectGenerationGroup, SWT.NONE);
projectGenerationControls.setLayout(new GridLayout(1, false));
projectGenerationControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
createGenerationOptions(projectGenerationControls);
setTopControl(projectGenerationControls);
return projectGenerationGroup;
}
protected void setTopControl(Control control)
{
projectGenerationStackLayout.topControl = control;
projectGenerationGroup.layout();
}
protected void createGenerationOptions(Composite projectGenerationControls)
{
gitCloneGenerate = new Button(projectGenerationControls, SWT.RADIO);
gitCloneGenerate.setText(Messages.WizardNewProjectCreationPage_CloneGitRepoLabel);
createGitLocationComposite(projectGenerationControls);
noGenerator = new Button(projectGenerationControls, SWT.RADIO);
if (getWizard().getPage(AbstractNewProjectWizard.TEMPLATE_SELECTION_PAGE_NAME) != null)
{
// we have a project-templates page
noGenerator.setText(Messages.WizardNewProjectCreationPage_NoGeneratorText2);
}
else
{
noGenerator.setText(Messages.WizardNewProjectCreationPage_NoGeneratorText);
}
noGenerator.setSelection(true);
noGenerator.addSelectionListener(new SelectionAdapter()
{
public void widgetSelected(SelectionEvent e)
{
// Trigger a buttons update. This is important for the enablement and disablement of the 'Next' button.
WizardNewRubyProjectCreationPage.this.getContainer().updateButtons();
}
});
}
private void createGitLocationComposite(Composite parent)
{
// project specification group
Composite projectGroup = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout();
layout.numColumns = 4;
projectGroup.setLayout(layout);
projectGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
// location label
Label locationLabel = new Label(projectGroup, SWT.NONE);
locationLabel.setText(Messages.ProjectLocationSelectionDialog_locationLabel);
// project location entry field
gitLocation = new SpecialText(projectGroup, SWT.BORDER | SWT.SINGLE)
{
@Override
protected boolean isDefault()
{
return matchesLastDefault();
}
@Override
protected boolean matchesLastDefault()
{
return gitLocationIsDefault();
}
};
GridData data = new GridData(GridData.FILL_HORIZONTAL);
data.widthHint = SIZING_TEXT_FIELD_WIDTH;
data.horizontalSpan = 2;
gitLocation.setLayoutData(data);
gitLocation.addModifyListener(new ModifyListener()
{
public void modifyText(ModifyEvent e)
{
setPageComplete(validatePage());
}
});
gitLocation.addFocusListener(new FocusAdapter()
{
@Override
public void focusGained(FocusEvent e)
{
selectGitCloneGeneration();
}
});
// browse button
gitBrowseButton = new Button(projectGroup, SWT.PUSH);
gitBrowseButton.setText(StringUtil.ellipsify(CoreStrings.BROWSE));
gitBrowseButton.addSelectionListener(new SelectionAdapter()
{
@Override
public void widgetSelected(SelectionEvent e)
{
handleGitLocationBrowseButtonPressed();
}
});
}
/**
* Returns the current project location path as entered by the user, or its anticipated initial value. Note that if
* the default has been returned the path in a project description used to create a project should not be set.
*
* @return the project location path or its anticipated initial value.
*/
public IPath getLocationPath()
{
if (useDefaults())
{
return Platform.getLocation();
}
return Path.fromOSString(locationPathField.getText());
}
/**
* /** Returns the current project location URI as entered by the user, or <code>null</code> if a valid project
* location has not been entered.
*
* @return the project location URI, or <code>null</code>
* @since 3.2
*/
public URI getLocationURI()
{
return getLocationPath().toFile().toURI();
}
/**
* Creates a project resource handle for the current project name field value. The project handle is created
* relative to the workspace root.
* <p>
* This method does not create the project resource; this is the responsibility of <code>IProject::create</code>
* invoked by the new project resource wizard.
* </p>
*
* @return the new project resource handle
*/
public IProject getProjectHandle()
{
return ResourcesPlugin.getWorkspace().getRoot().getProject(getProjectName());
}
/**
* Returns the current project name as entered by the user, or its anticipated initial value.
*
* @return the project name, its anticipated initial value, or <code>null</code> if no project name is known
*/
public String getProjectName()
{
if (projectNameField == null)
{
return initialProjectFieldValue;
}
return getProjectNameFieldValue();
}
/**
* Returns the value of the project name field with leading and trailing spaces removed.
*
* @return the project name in the field
*/
private String getProjectNameFieldValue()
{
if (projectNameField == null)
{
return EMPTY_STRING;
}
return projectNameField.getText().trim();
}
/**
* Sets the initial project name that this page will use when created. The name is ignored if the
* createControl(Composite) method has already been called. Leading and trailing spaces in the name are ignored.
* Providing the name of an existing project will not necessarily cause the wizard to warn the user. Callers of this
* method should first check if the project name passed already exists in the workspace.
*
* @param name
* initial project name for this page
* @see IWorkspace#validateName(String, int)
*/
public void setInitialProjectName(String name)
{
if (name == null)
{
initialProjectFieldValue = null;
}
else
{
initialProjectFieldValue = name.trim();
updateProjectName(name.trim());
}
}
private void updateProjectName(String trim)
{
// Update the location field
// Only update if the location field is empty, or is the "default" value
if (locationPathField.getText().trim().length() == 0 || locationMatchesLastDefault())
{
lastLocationDefault = Platform.getLocation().append(trim).toOSString();
locationPathField.setText(lastLocationDefault);
}
// Update the git location field
String username = System.getProperty("user.name"); //$NON-NLS-1$
if (username == null || username.length() == 0)
username = "user"; //$NON-NLS-1$
if (gitLocation.getText().trim().length() == 0 || gitLocationIsDefault())
{
lastGitDefault = MessageFormat.format("git://github.com/{0}/{1}.git", username, trim); //$NON-NLS-1$
gitLocation.setText(lastGitDefault);
}
}
private boolean locationMatchesLastDefault()
{
return lastLocationDefault != null && locationPathField.getText().trim().equals(lastLocationDefault);
}
private boolean gitLocationIsDefault()
{
return lastGitDefault != null && gitLocation.getText().trim().equals(lastGitDefault);
}
/**
* Returns whether this page's controls currently all contain valid values.
*
* @return <code>true</code> if all controls are valid, and <code>false</code> if at least one is invalid
*/
protected boolean validatePage()
{
projectGenerationStackLayout.topControl = projectGenerationControls;
projectGenerationGroup.layout();
IWorkspace workspace = ResourcesPlugin.getWorkspace();
String projectFieldContents = getProjectNameFieldValue();
if (projectFieldContents.equals(EMPTY_STRING))
{
setErrorMessage(null);
setMessage(Messages.WizardNewProjectCreationPage_projectNameEmpty);
return false;
}
IStatus nameStatus = workspace.validateName(projectFieldContents, IResource.PROJECT);
if (!nameStatus.isOK())
{
setErrorMessage(nameStatus.getMessage());
return false;
}
IProject handle = getProjectHandle();
if (handle.exists())
{
setErrorMessage(Messages.WizardNewProjectCreationPage_projectExistsMessage);
return false;
}
String validLocationMessage = checkValidLocation();
if (validLocationMessage != null)
{ // there is no destination location given
setErrorMessage(validLocationMessage);
return false;
}
validLocationMessage = checkValidGitLocation();
if (validLocationMessage != null)
{
setErrorMessage(validLocationMessage);
return false;
}
if (warningLabel != null)
{
warningLabel.setText(StringUtil.EMPTY);
}
File locationFile = getLocationPath().toFile();
if (!useDefaults() && locationFile.exists())
{
String[] files = locationFile.list();
if (files != null && files.length > 0)
{
warningLabel.setText(Messages.WizardNewProjectCreationPage_location_has_existing_content_warning);
warningLabel.getParent().layout(true);
}
}
setErrorMessage(null);
setMessage(null);
return true;
}
/*
* see @DialogPage.setVisible(boolean)
*/
public void setVisible(boolean visible)
{
super.setVisible(visible);
if (visible)
{
projectNameField.setFocus();
}
}
/*
* (non-Javadoc)
* @see com.aptana.projects.internal.wizards.IWizardProjectCreationPage#isCloneFromGit()
*/
public boolean isCloneFromGit()
{
return gitCloneGenerate.getSelection();
}
/*
* (non-Javadoc)
* @see com.aptana.projects.internal.wizards.IWizardProjectCreationPage#getCloneURI()
*/
public String getCloneURI()
{
return gitLocation.getText().trim();
}
/*
* (non-Javadoc)
* @see com.aptana.projects.internal.wizards.IWizardProjectCreationPage#useDefaults()
*/
public boolean useDefaults()
{
// IPath platformDefaultLocation = Platform.getLocation();
// getLocationPath().toOSString();
// String defaultLocation = Platform.getLocation().append(getProjectNameFieldValue()).toOSString();
// return getLocationPath().toOSString().equals(defaultLocation);
IPath platformDefaultLocation = Platform.getLocation();
IPath selectedPath = Path.fromOSString(locationPathField.getText().trim());
return platformDefaultLocation.isPrefixOf(selectedPath);
}
protected void selectGitCloneGeneration()
{
noGenerator.setSelection(false);
gitCloneGenerate.setSelection(true);
}
private abstract class SpecialText extends StyledText
{
protected boolean justGainedFocus;
public SpecialText(Composite parent, int style)
{
super(parent, style);
addModifyListener(new ModifyListener()
{
/*
* (non-Javadoc)
* @see org.eclipse.swt.events.ModifyListener#modifyText(org.eclipse.swt.events.ModifyEvent)
*/
public void modifyText(ModifyEvent e)
{
if (!matchesLastDefault())
{
setStyleRange(new StyleRange(0, getCharCount(), null, null));
}
else
{
setStyleRange(new StyleRange(0, getCharCount(), getShell().getDisplay().getSystemColor(
SWT.COLOR_DARK_GRAY), null, SWT.ITALIC));
}
}
});
addFocusListener(new FocusAdapter()
{
@Override
public void focusGained(FocusEvent e)
{
super.focusGained(e);
if (isDefault())
{
selectAll();
justGainedFocus = true;
}
}
});
addMouseListener(new MouseListener()
{
public void mouseUp(MouseEvent e)
{
}
public void mouseDown(MouseEvent e)
{
if (justGainedFocus)
{
justGainedFocus = false;
// FIXME Don't do this if everything was already selected! I don't know how to see that since at
// this point the selection has already changed!
selectAll();
}
}
public void mouseDoubleClick(MouseEvent e)
{
}
});
}
abstract protected boolean matchesLastDefault();
abstract protected boolean isDefault();
}
public String getStepName()
{
return Messages.NewRubyProject_stepName;
}
public void initStepIndicator(String[] stepNames)
{
this.stepNames = stepNames;
}
}