/*******************************************************************************
* Copyright (c) 2014 Arapiki Solutions Inc.
* 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:
* psmith - initial API and
* implementation and/or initial documentation
*******************************************************************************/
package com.buildml.eclipse.utils.dialogs;
import java.util.ArrayList;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import com.buildml.eclipse.utils.BmlTitleAreaDialog;
import com.buildml.eclipse.utils.errors.FatalError;
import com.buildml.model.IBuildStore;
import com.buildml.model.IPackageMgr;
import com.buildml.model.ISlotTypes;
import com.buildml.model.ISlotTypes.SlotDetails;
import com.buildml.utils.string.BuildStoreUtils;
/**
* A dialog allowing the user to add/modify a slot's definitions.
*
* @author Peter Smith <psmith@arapiki.com>
*/
public class SlotDefinitionDialog extends BmlTitleAreaDialog {
/*=====================================================================================*
* FIELDS/TYPES
*=====================================================================================*/
/** The IBuildStore */
private IBuildStore buildStore;
/** True if this Dialog is being used to edit a newly-created slot definition */
private boolean createNew;
/** The slot details to display/edit */
private SlotDetails details;
/** All the slots that are currently active for this package/action-type */
private ArrayList<SlotDetails> allSlots;
/** The Text Control for entering/editing the slot name */
private Text slotNameEntry;
/** The Text Control for entering/editing the default value */
private Text defaultValueEntry;
/** The Text Control for entering/editing the slot description */
private Text descrEntry;
/** The Combo box for selecting the type of the slot (e.g. Text, Integer, etc) */
private Combo typeCombo;
/** The Combo box for selecting the cardinality of the slot (e.g. optional, required) */
private Combo cardCombo;
/*=====================================================================================*
* CONSTRUCTOR
*=====================================================================================*/
/**
* Create a new {@link SlotDefinitionDialog}
*
* @param buildStore The IBuildStore that the slot is part of.
* @param createNew True if we're creating (and editing) a new slot.
* @param details The existing (or default) slot details to edit.
* @param allSlots All of the slots for this package/action-type.
*/
public SlotDefinitionDialog(IBuildStore buildStore, boolean createNew,
SlotDetails details, ArrayList<SlotDetails> allSlots) {
super(new Shell(), 0.3, 0.5, 0.5, 0.5);
this.buildStore = buildStore;
this.createNew = createNew;
this.details = details;
this.allSlots = allSlots;
}
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/**
* Return the slot's details. This should only be called after the "OK" button has been
* pressed.
*
* @return The slot's details.
*/
public SlotDetails getSlotDetails() {
return details;
}
/*=====================================================================================*
* PROTECTED METHODS
*=====================================================================================*/
/* (non-Javadoc)
* @see com.buildml.eclipse.utils.BmlTitleAreaDialog#createDialogArea(org.eclipse.swt.widgets.Composite)
*/
@Override
protected Control createDialogArea(Composite parent) {
/*
* Create the main panel widget for the body of the dialog box.
* There are 2 columns: 1) The labels, 2) The field entries.
*/
Composite panel = new Composite(parent, SWT.NONE);
panel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
GridLayout layout = new GridLayout();
layout.marginHeight = 10;
layout.marginWidth = 20;
layout.verticalSpacing = 10;
layout.numColumns = 2;
panel.setLayout(layout);
/*
* Title - spans two columns.
*/
Label titleLabel = new Label(panel, SWT.None);
GridData gd = new GridData();
gd.horizontalSpan = 2;
titleLabel.setLayoutData(gd);
titleLabel.setText("Specify the name and type information for this slot:\n");
/* the behaviour of some fields may vary, depending on slot's position */
boolean isInputOutputSlot = (details.slotPos == ISlotTypes.SLOT_POS_INPUT) ||
(details.slotPos == ISlotTypes.SLOT_POS_OUTPUT);
/*
* Create the various form fields. All field validation and behaviour is
* encapsulated in these methods.
*/
createNameEntryField(panel);
createSlotPosField(panel);
createSlotTypeField(panel, isInputOutputSlot);
createDefaultValueEntryField(panel, isInputOutputSlot);
createCardField(panel, details.slotPos);
createDescrField(panel);
return parent;
}
/*-------------------------------------------------------------------------------------*/
/*
* The "OK" button has been pressed - fetch the field value out of the Control (which
* will soon be destroyed) and store them for later retrieval by the getSlotDetails()
* method.
*/
@Override
protected void okPressed() {
/* fetch the new slotName field */
details.slotName = slotNameEntry.getText();
/* fetch the new slotType field */
if (typeCombo != null) {
details.slotType = getSlotTypeFromName(typeCombo.getText());
}
/* fetch the defaultValue field */
if (defaultValueEntry != null) {
details.defaultValue = getDefaultValueAsObject(defaultValueEntry.getText());
} else {
details.defaultValue = null;
}
/* fetch the slotCard field */
details.slotCard = getSlotCardFromName(cardCombo.getText());
/* fetch the slotDescr field */
details.slotDescr = descrEntry.getText().trim();
/* now close the dialog box */
super.okPressed();
}
/*-------------------------------------------------------------------------------------*/
/*
* Ensure that the OK button is initially grey-out when we're creating a new slot.
* This encourages the user to replace the default name.
*/
@Override
protected Control createButtonBar(Composite parent) {
Control buttons = super.createButtonBar(parent);
if (createNew) {
getButton(OK).setEnabled(false);
}
return buttons;
}
/*-------------------------------------------------------------------------------------*/
/*
* (non-Javadoc)
* @see org.eclipse.jface.window.Window#configureShell(org.eclipse.swt.widgets.Shell)
*/
@Override
protected void configureShell(Shell newShell) {
super.configureShell(newShell);
newShell.setText("Edit Slot Details");
}
/*=====================================================================================*
* PRIVATE METHODS
*=====================================================================================*/
/**
* This method is the central location for validating field values, updating the
* error message, and enabling the OK button.
*/
private void updateOkAndErrorMessage() {
/* assume the OK button should be enabled, and assume there's no error message */
String errorMsg = null;
/* validate the defaultValue field */
if (defaultValueEntry != null) {
if (!validateDefaultValue(defaultValueEntry.getText())) {
errorMsg = "Default value is not valid for this slot type.";
}
}
/* validate the slotName field */
String name = slotNameEntry.getText();
boolean valid = BuildStoreUtils.isValidSlotName(name);
boolean slotNameInUse = isSlotNameAlreadyUsed(name);
if (!valid) {
errorMsg = "Slot name is invalid (too short, or invalid characters)";
} else if (slotNameInUse) {
errorMsg = "Name is already in use by another slot";
}
/* finally, if any of the above tests reported a problem, set the error message and disable OK */
setErrorMessage(errorMsg);
getButton(OK).setEnabled(errorMsg == null);
}
/*-------------------------------------------------------------------------------------*/
/**
* Create the "slot name" entry field. For new slots, we encourage the user to change
* the default name immediately. Any changes to the slot name will be evaluated to
* ensure that the name is valid, and it's not already in use by another slot.
*
* @param panel The parent Composite to add the field to.
*/
private void createNameEntryField(Composite panel) {
new Label(panel, SWT.None).setText("Name:");
slotNameEntry = new Text(panel, SWT.None);
slotNameEntry.setLayoutData(new GridData(SWT.FILL, SWT.None, true, false));
slotNameEntry.setText(details.slotName);
if (createNew) {
slotNameEntry.selectAll();
}
/* on every change to the slotName, report on whether the name is good */
slotNameEntry.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
updateOkAndErrorMessage();
}
});
}
/*-------------------------------------------------------------------------------------*/
/**
* Create the "slot position" field. This can never be edited, so it's done with static labels.
*
* @param panel The parent Composite to add the field to.
*/
private void createSlotPosField(Composite panel) {
IPackageMgr pkgMgr = buildStore.getPackageMgr();
new Label(panel, SWT.None).setText("Position:");
/* determine the owner (action or package) */
String ownerString;
if (details.ownerType == ISlotTypes.SLOT_OWNER_PACKAGE) {
String pkgName = pkgMgr.getName(details.ownerId);
ownerString = "\"" + pkgName + "\" Package";
}
else if (details.ownerType == ISlotTypes.SLOT_OWNER_ACTION) {
ownerString = "Action - ";
}
else {
ownerString = "<not defined>";
}
/* determine the slot position */
String slotPosName = getSlotPosName(details.slotPos);
/* display an owner/position string */
new Label(panel, SWT.None).setText(ownerString + " - " + slotPosName + " Slot");
}
/*-------------------------------------------------------------------------------------*/
/**
* Create the "slot type" field. This can only be edited for new slots that haven't
* yet been added to the BuildStore, otherwise we use labels.
*
* @param panel The parent Composite to add the field to.
* @param isInputOutputSlot True if this is an input/output slot, else false.
*/
private void createSlotTypeField(Composite panel, boolean isInputOutputSlot) {
new Label(panel, SWT.None).setText("Type:");
/* for newly-created parameter/local slots, the user can choose the type */
if ((details.slotId == -1) && !isInputOutputSlot) {
typeCombo = new Combo(panel, SWT.READ_ONLY);
typeCombo.add(getSlotTypeName(ISlotTypes.SLOT_TYPE_TEXT));
typeCombo.add(getSlotTypeName(ISlotTypes.SLOT_TYPE_INTEGER));
typeCombo.add(getSlotTypeName(ISlotTypes.SLOT_TYPE_BOOLEAN));
typeCombo.add(getSlotTypeName(ISlotTypes.SLOT_TYPE_FILE));
typeCombo.add(getSlotTypeName(ISlotTypes.SLOT_TYPE_DIRECTORY));
int index = getSlotTypeIndex(details.slotType);
if (index != -1) {
typeCombo.select(index);
}
/*
* Update the "default value" field based on a change in the selection.
*/
typeCombo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
int selectedType = getSlotTypeFromName(typeCombo.getText());
switch(selectedType) {
case ISlotTypes.SLOT_TYPE_BOOLEAN:
defaultValueEntry.setText("false");
defaultValueEntry.setEnabled(true);
break;
case ISlotTypes.SLOT_TYPE_TEXT:
defaultValueEntry.setText("");
defaultValueEntry.setEnabled(true);
break;
case ISlotTypes.SLOT_TYPE_INTEGER:
defaultValueEntry.setText("0");
defaultValueEntry.setEnabled(true);
break;
default:
defaultValueEntry.setText("");
defaultValueEntry.setEnabled(false);
break;
}
}
});
}
/* else, it's fixed and we can't change it */
else {
String slotTypeName = getSlotTypeName(details.slotType);
new Label(panel, SWT.None).setText(slotTypeName);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Create the "default value" field. This is only relevant for text, integer and
* boolean values, otherwise it's greyed-out.
*
* @param panel The parent Composite to add the field to.
* @param isInputOutputSlot True if this is an input/output slot, else false.
*/
private void createDefaultValueEntryField(Composite panel, boolean isInputOutputSlot) {
/*
* Only parameter/local slots can have default values, and for File and Directory
* slots, we don't bother showing this field at all.
*/
if (!isInputOutputSlot &&
(createNew ||
((details.slotType != ISlotTypes.SLOT_TYPE_FILE) &&
(details.slotType != ISlotTypes.SLOT_TYPE_DIRECTORY)))) {
new Label(panel, SWT.None).setText("Default Value:");
defaultValueEntry = new Text(panel, SWT.None);
defaultValueEntry.setLayoutData(new GridData(SWT.FILL, SWT.None, true, false));
defaultValueEntry.setText((details.defaultValue == null) ? "" : details.defaultValue.toString());
/* validate the text, to make sure it's valid for the current slotType */
defaultValueEntry.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
updateOkAndErrorMessage();
}
});
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Create the "cardinality" field. For input/output, this can be optional/required/multi.
* For parameters and locals, only optional/required make sense.
*
* @param panel The parent Composite to add the field to.
* @param slotPos The position of this slot (e.g. SLOT_POS_INPUT).
*/
private void createCardField(Composite panel, int slotPos) {
new Label(panel, SWT.None).setText("Cardinality:");
cardCombo = new Combo(panel, SWT.READ_ONLY);
cardCombo.add("Optional");
cardCombo.add("Required");
if (slotPos == ISlotTypes.SLOT_POS_INPUT) {
cardCombo.add("Multi-Slot");
}
/* set the combo box to reflect the slot's current state */
int slotCard = details.slotCard;
cardCombo.select((slotCard == ISlotTypes.SLOT_CARD_OPTIONAL) ? 0 :
(slotCard == ISlotTypes.SLOT_CARD_REQUIRED) ? 1 : 2);
}
/*-------------------------------------------------------------------------------------*/
/**
* Create the "description" field.
*
* @param panel The parent Composite to add the field to.
*/
private void createDescrField(Composite panel) {
new Label(panel, SWT.NONE).setText("Description:");
descrEntry = new Text(panel, SWT.MULTI | SWT.BORDER);
descrEntry.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
descrEntry.setText(details.slotDescr);
}
/*-------------------------------------------------------------------------------------*/
/**
* Given a proposed slot name, check if it's already used for any other slots
* in this package/action-type.
*
* @param name The proposed name.
* @return True if the name is already used, else false.
*/
private boolean isSlotNameAlreadyUsed(String name) {
/* it's OK if the name is set back to what it was when we started editing */
if (name.equals(details.slotName)) {
return false;
}
/* otherwise check all of the existing slots to make sure we're not duplicating slot names */
for (SlotDetails slot : allSlots) {
if (name.equals(slot.slotName)) {
return true;
}
}
return false;
}
/*-------------------------------------------------------------------------------------*/
/**
* Given a slotPos number, return the corresponding String name.
*
* @param slotPos The slotPos number.
* @return The corresponding String name.
*/
private String getSlotPosName(int slotPos) {
return (slotPos == ISlotTypes.SLOT_POS_INPUT) ? "Input" :
(slotPos == ISlotTypes.SLOT_POS_LOCAL) ? "Local" :
(slotPos == ISlotTypes.SLOT_POS_PARAMETER) ? "Parameter" :
(slotPos == ISlotTypes.SLOT_POS_OUTPUT) ? "Output" : "<invalid>";
}
/*-------------------------------------------------------------------------------------*/
/**
* Given a slotType number, return the corresponding String name.
*
* @param slotType The slotType number.
* @return The corresponding String name.
*/
private String getSlotTypeName(int slotType) {
switch (slotType) {
case ISlotTypes.SLOT_TYPE_BOOLEAN:
return "Boolean";
case ISlotTypes.SLOT_TYPE_DIRECTORY:
return "Directory";
case ISlotTypes.SLOT_TYPE_FILE:
return "File";
case ISlotTypes.SLOT_TYPE_FILEGROUP:
return "FileGroup";
case ISlotTypes.SLOT_TYPE_INTEGER:
return "Integer";
case ISlotTypes.SLOT_TYPE_TEXT:
return "Text";
default:
return "<invalid>";
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Given a slotType number, return the corresponding index within the combo box.
*
* @param slotType The slotType number.
* @return The corresponding index in the combo box, or -1 if not shown in combo box.
*/
private int getSlotTypeIndex(int slotType) {
switch (slotType) {
case ISlotTypes.SLOT_TYPE_TEXT:
return 0;
case ISlotTypes.SLOT_TYPE_INTEGER:
return 1;
case ISlotTypes.SLOT_TYPE_BOOLEAN:
return 2;
case ISlotTypes.SLOT_TYPE_FILE:
return 3;
case ISlotTypes.SLOT_TYPE_DIRECTORY:
return 4;
default:
return -1;
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Given a slot type name, return the corresponding ISlotTypes value. A valid type name
* must be provided.
*
* @param typeName Textual name of the slot type (e.g "Integer").
* @return The corresponding ISlotTypes value.
*/
private int getSlotTypeFromName(String typeName) {
if (typeName.equals("Boolean")) {
return ISlotTypes.SLOT_TYPE_BOOLEAN;
} else if (typeName.equals("Directory")) {
return ISlotTypes.SLOT_TYPE_DIRECTORY;
} else if (typeName.equals("File")) {
return ISlotTypes.SLOT_TYPE_FILE;
} else if (typeName.equals("FileGroup")) {
return ISlotTypes.SLOT_TYPE_FILEGROUP;
} else if (typeName.equals("Integer")) {
return ISlotTypes.SLOT_TYPE_INTEGER;
} else if (typeName.equals("Text")) {
return ISlotTypes.SLOT_TYPE_TEXT;
}
throw new FatalError("Invalid typeName");
}
/*-------------------------------------------------------------------------------------*/
/**
* Given the name of a slot cardinality, return the corresponding ISlotTypes constant.
* @param name The name of the cardinality.
* @return The corresponding ISlotTypes constant.
*/
private int getSlotCardFromName(String name) {
if (name.equals("Optional")) {
return ISlotTypes.SLOT_CARD_OPTIONAL;
} else if (name.equals("Required")) {
return ISlotTypes.SLOT_CARD_REQUIRED;
} else if (name.equals("Multi-Slot")) {
return ISlotTypes.SLOT_CARD_MULTI;
}
throw new FatalError("Invalid name");
}
/*-------------------------------------------------------------------------------------*/
/**
* Based on the currently selected "slotType", determine whether the default value
* field currently holds a valid value.
*
* @param value The string that's currently in the "default value" field.
* @return True if the default field is valid, else false.
*/
private boolean validateDefaultValue(String value) {
/* determine the slot's type, either from the type combo, or the predefined details */
int slotType;
if (typeCombo != null) {
slotType = getSlotTypeFromName(typeCombo.getText());
} else {
slotType = details.slotType;
}
/* based on the slot type, check if the default value is valid */
switch(slotType) {
case ISlotTypes.SLOT_TYPE_BOOLEAN:
return value.equals("true") || value.equals("false");
case ISlotTypes.SLOT_TYPE_INTEGER:
try {
Integer.valueOf(value);
return true;
} catch (NumberFormatException ex) {
return false;
}
case ISlotTypes.SLOT_TYPE_TEXT:
return true;
default:
return true;
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Convert the default value from a String to an Object of the relevant type.
*
* @param stringValue The default value as a String.
* @return The default value as an Object.
*/
private Object getDefaultValueAsObject(String stringValue) {
/* determine the slot's type, either from the type combo, or the predefined details */
int slotType;
if (typeCombo != null) {
slotType = getSlotTypeFromName(typeCombo.getText());
} else {
slotType = details.slotType;
}
switch(slotType) {
case ISlotTypes.SLOT_TYPE_BOOLEAN:
if (stringValue.equals("true")) {
return Boolean.TRUE;
} else {
return Boolean.FALSE;
}
case ISlotTypes.SLOT_TYPE_INTEGER:
return Integer.valueOf(stringValue);
case ISlotTypes.SLOT_TYPE_TEXT:
return stringValue;
default:
return null;
}
}
/*-------------------------------------------------------------------------------------*/
}