/******************************************************************************* * 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.properties; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ScrolledComposite; 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.events.SelectionListener; 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.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; import com.buildml.eclipse.bobj.UIInteger; import com.buildml.eclipse.bobj.UISubPackage; import com.buildml.eclipse.utils.BmlPropertyPage; import com.buildml.eclipse.utils.EclipsePartUtils; import com.buildml.eclipse.utils.GraphitiUtils; import com.buildml.eclipse.utils.UndoOpAdapter; import com.buildml.eclipse.utils.dialogs.VFSTreeSelectionDialog; import com.buildml.model.IBuildStore; import com.buildml.model.IFileMgr; import com.buildml.model.IPackageMgr; import com.buildml.model.ISlotTypes; import com.buildml.model.ISlotTypes.SlotDetails; import com.buildml.model.undo.MultiUndoOp; import com.buildml.model.undo.SlotUndoOp; import com.buildml.model.ISubPackageMgr; /** * An Eclipse "property" page that allows viewing/editing of a sub-packages properties. * * Objects of this class are referenced in the plugin.xml file and are dynamically * created when the properties dialog is opened for a UISubPackage object. * * @author Peter Smith <psmith@arapiki.com> */ public class SlotValuePropertyPage extends BmlPropertyPage { /*=====================================================================================* * NESTED CLASSES *=====================================================================================*/ /** * A nested class to be used by create*Editor methods to provide functionality for: * 1) Validating each field's current value (as entered by the user into a TextBox) * 2) Scheduling the necessary undo/redo work when "OK" is pressed. * 3) Restoring the field's default values. */ private abstract class Validator { /** * @return null if the field's value is valid, else an error message. */ abstract String getValidationError(); /** * Schedule the necessary undo/redo work if this field's value has been user-modified. * @param multiOp The MultiOp to add the changes to. */ abstract void scheduleChange(MultiUndoOp multiOp); /** * Ask each field to restore itself to its default values. */ abstract void restoreDefaults(); } /*=====================================================================================* * CONSTANTS *=====================================================================================*/ /** The number of lines that should appear when we're editing a text-typed slot */ public static final int LINES_FOR_TEXT_BOX = 5; /*=====================================================================================* * FIELDS/TYPES *=====================================================================================*/ /** Manager that holds package details */ private IPackageMgr pkgMgr; /** Manager that holds sub-package details */ private ISubPackageMgr subPkgMgr; /** Manager that holds sub-package details */ private IFileMgr fileMgr; /** The validators that check the content of each entry field */ private List<Validator> validators; /** The ID of the sub-package we're editing */ private int subPkgId; /*=====================================================================================* * CONSTRUCTORS *=====================================================================================*/ /** * Create a new SubPackagePropertyPage() object. */ public SlotValuePropertyPage() { validators = new ArrayList<SlotValuePropertyPage.Validator>(); } /*=====================================================================================* * PUBLIC METHODS *=====================================================================================*/ /** * The OK button has been pressed in the properties box. Save all the field values * into the database. This is done via the undo/redo stack. Each create*Editor method * provides a Validator object that schedules the necessary undo/redo operations. */ @Override public boolean performOk() { MultiUndoOp multiOp = new MultiUndoOp(); for (Validator validator : validators) { validator.scheduleChange(multiOp); } if (multiOp.size() > 0) { new UndoOpAdapter("Edit Slots", multiOp).invoke(); } return super.performOk(); } /*-------------------------------------------------------------------------------------*/ /** * The "restore default" button has been pressed, so return everything back to its * original state. */ @Override protected void performDefaults() { for (Validator validator : validators) { validator.restoreDefaults(); } super.performDefaults(); } /*=====================================================================================* * PROTECTED METHODS *=====================================================================================*/ /** * Create the widgets that appear within the properties dialog box. For this property * page, we list all of the slots, along with their associated values. */ @Override protected Control createContents(Composite parent) { IBuildStore buildStore = EclipsePartUtils.getActiveBuildStore(); pkgMgr = buildStore.getPackageMgr(); subPkgMgr = buildStore.getSubPackageMgr(); fileMgr = buildStore.getFileMgr(); /* * Determine which "thing" (UISubPackage or UIAction) we're looking at, then * compute the corresponding pkgId or actionTypeId that this "thing" is an * instance of. */ UISubPackage subPkg = (UISubPackage) GraphitiUtils.getBusinessObjectFromElement(getElement(), UISubPackage.class); if (subPkg == null) { return null; } subPkgId = subPkg.getId(); int pkgId = subPkgMgr.getSubPackageType(subPkgId); if (pkgId < 0) { return null; } /* fetch the "parameter" slots, and sort them alphabetically */ SlotDetails[] paramSlots = pkgMgr.getSlots(pkgId, ISlotTypes.SLOT_POS_PARAMETER); Arrays.sort(paramSlots,new Comparator<SlotDetails>() { @Override public int compare(SlotDetails arg0, SlotDetails arg1) { return arg0.slotName.compareTo(arg1.slotName); } }); /* prepare the top-level Composite in which everything else is placed */ setTitle("Sub-Package Properties:"); /* put everything inside a scrolled composite, in case we have a very long list of slots */ ScrolledComposite scrolledComposite = new ScrolledComposite(parent, SWT.BORDER | SWT.V_SCROLL); scrolledComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); scrolledComposite.setLayout(new GridLayout()); scrolledComposite.setAlwaysShowScrollBars(true); /* create a sub-composite that contains all the slot details */ Composite panel = new Composite(scrolledComposite, SWT.NONE); panel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); GridLayout layout = new GridLayout(); layout.marginHeight = 0; layout.marginWidth = 0; panel.setLayout(layout); /* * For each slot, display a group box, the slot's name and description, * and then an appropriate set of controls to allow editing of the * field's value. */ for (int i = 0; i < paramSlots.length; i++) { SlotDetails details = paramSlots[i]; /* add group-box around each slot's information */ Group group = new Group(panel, SWT.BORDER); group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); group.setLayout(new GridLayout()); /* display the slot title - centered and bolded */ Label title = new Label(group, SWT.CENTER); title.setLayoutData(new GridData(SWT.CENTER, SWT.None, true, false)); title.setText(details.slotName); title.setFont(JFaceResources.getFontRegistry().getBold("")); /* display the description - wrapped */ Label descr = new Label(group, SWT.WRAP); GridData gd = new GridData(GridData.FILL_BOTH); gd.widthHint = EclipsePartUtils.getScreenWidth() / 3; descr.setLayoutData(gd); descr.setText(details.slotDescr); /* fetch the slot's current value */ boolean isSlotSet = subPkgMgr.isSlotSet(subPkgId, details.slotId); Object slotValue = subPkgMgr.getSlotValue(subPkgId, details.slotId); /* * For each slot type, provide the ability to edit the current value. */ switch (details.slotType) { /* Integer-typed slots */ case ISlotTypes.SLOT_TYPE_INTEGER: createIntegerEditor(group, details, isSlotSet, (Integer)details.defaultValue, (Integer)slotValue); break; /* Text-typed slots */ case ISlotTypes.SLOT_TYPE_TEXT: createTextEditor(group, details, isSlotSet, (String)details.defaultValue, (String)slotValue); break; /* Boolean-typed slots */ case ISlotTypes.SLOT_TYPE_BOOLEAN: createBooleanEditor(group, details, isSlotSet, (Boolean)details.defaultValue, (Boolean)slotValue); break; /* File-typed slots */ case ISlotTypes.SLOT_TYPE_FILE: createFileDirEditor(group, details, isSlotSet, (Integer)details.defaultValue, (Integer)slotValue, true); break; /* Directory-typed slots */ case ISlotTypes.SLOT_TYPE_DIRECTORY: createFileDirEditor(group, details, isSlotSet, (Integer)details.defaultValue, (Integer)slotValue, false); break; default: /* do nothing */ } } /* tell the ScrolledComposite what it's managing, and how big it should be. */ scrolledComposite.setContent(panel); scrolledComposite.setExpandHorizontal(true); scrolledComposite.setMinWidth(0); panel.setSize(panel.computeSize(SWT.DEFAULT, SWT.DEFAULT)); return scrolledComposite; } /*=====================================================================================* * PRIVATE METHODS *=====================================================================================*/ /** * Create the Controls necessary for editing a Text-typed slot. * * @param group The parent group panel to add Control into. * @param details The slot's details. * @param isSlotSet True if the slot currently has a value set, or false if the default * value would be used. * @param defaultValue The default value for the slot. * @param slotValue The slot's current value (or null if !isSlotSet). */ private void createTextEditor(Composite group, final SlotDetails details, final boolean isSlotSet, final String defaultValue, final String slotValue) { /* * First, show the textBox, which should extend over multiple lines. */ final Text textBox = new Text(group, SWT.BORDER | SWT.MULTI); GridData gd = new GridData(SWT.FILL, SWT.FILL, true, false); gd.heightHint = EclipsePartUtils.getFontHeight(textBox) * LINES_FOR_TEXT_BOX; textBox.setLayoutData(gd); /* * Add the "Use Default" check-button. If it's checked, then we showed * the slot's default value in the textBox and un-enabled the textBox. */ final Button checkButton = addDefaultCheckbox(group, isSlotSet); final SelectionListener select = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { boolean useDefault = checkButton.getSelection(); textBox.setEnabled(!useDefault); if (useDefault) { textBox.setText(defaultValue); } else { textBox.setText(isSlotSet ? slotValue : ""); } }}; checkButton.addSelectionListener(select); select.widgetSelected(null); /* * Register a validator for this field. */ registerField(new Validator() { /* any String value is OK - never any errors given */ @Override String getValidationError() { return null; } /* If this field has changed, schedule the undo/redo operation */ @Override void scheduleChange(MultiUndoOp multiOp) { boolean newExists = !checkButton.getSelection(); String newValue = newExists ? textBox.getText().trim() : slotValue; if ((newExists != isSlotSet) || !(slotValue.equals(newValue))) { SlotUndoOp op = new SlotUndoOp(buildStore, ISlotTypes.SLOT_OWNER_PACKAGE); op.recordChangeSlotValue(subPkgId, details.slotId, isSlotSet, slotValue, newExists, newValue); multiOp.add(op); } } /* restore default values */ @Override void restoreDefaults() { checkButton.setSelection(!isSlotSet); select.widgetSelected(null); } }); } /*-------------------------------------------------------------------------------------*/ /** * Create the Controls necessary for editing an Integer-typed slot. * * @param group The parent group panel to add Control into. * @param details The slot's details. * @param isSlotSet True if the slot currently has a value set, or false if the default * value would be used. * @param defaultValue The default value for the slot. * @param slotValue The slot's current value (or null if !isSlotSet). */ private void createIntegerEditor(Group group, final SlotDetails details, final boolean isSlotSet, final Integer defaultValue, final Integer slotValue) { /* * First, show the textBox. */ final Text integerBox = new Text(group, SWT.BORDER); integerBox.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); /* * Add the "Use Default" check-button. If it's checked, then we showed * the slot's default value in the textBox and un-enabled the textBox. */ final Button checkButton = addDefaultCheckbox(group, isSlotSet); final SelectionListener select = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { boolean useDefault = checkButton.getSelection(); integerBox.setEnabled(!useDefault); if (useDefault) { integerBox.setText(defaultValue.toString()); } else { integerBox.setText(isSlotSet ? slotValue.toString() : "0"); } }}; checkButton.addSelectionListener(select); select.widgetSelected(null); /* * Register a validator for this field. */ registerField(new Validator() { /* Validate the text box, and return an error message */ @Override String getValidationError() { String s = integerBox.getText(); for (int i = 0; i != s.length(); i++) { if (!Character.isDigit(s.charAt(i))) { return details.slotName + ": Non-numeric characters not allowed."; } } return null; } /* If our fields changed, schedule the necessary undo/redo operation to happen */ @Override void scheduleChange(MultiUndoOp multiOp) { boolean newExists = !checkButton.getSelection(); Integer newValue = newExists ? Integer.valueOf(integerBox.getText()) : slotValue; if ((newExists != isSlotSet) || !(slotValue.equals(newValue))) { SlotUndoOp op = new SlotUndoOp(buildStore, ISlotTypes.SLOT_OWNER_PACKAGE); op.recordChangeSlotValue(subPkgId, details.slotId, isSlotSet, slotValue, newExists, newValue); multiOp.add(op); } } /* restore default values */ @Override void restoreDefaults() { checkButton.setSelection(!isSlotSet); select.widgetSelected(null); } }); /* * Monitor edits to the integerBox, to make sure it always contains a valid * integer value. */ integerBox.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { validateFields(); } }); } /*-------------------------------------------------------------------------------------*/ /** * Create the Controls necessary for editing a Boolean-typed slot. * * @param group The parent group panel to add Control into. * @param details The slot's details. * @param isSlotSet True if the slot currently has a value set, or false if the default * value would be used. * @param defaultValue The default value for the slot. * @param slotValue The slot's current value (or null if !isSlotSet). */ private void createBooleanEditor(Group group, final SlotDetails details, final boolean isSlotSet, final Boolean defaultValue, final Boolean slotValue) { /* Show a pair of radio buttons - "true" and "false" */ Composite choices = new Composite(group, SWT.NONE); choices.setLayoutData(new GridData(SWT.FILL, SWT.None, true, false)); GridLayout layout = new GridLayout(2, false); layout.marginHeight = layout.verticalSpacing = 0; choices.setLayout(layout); final Button trueButton = new Button(choices, SWT.RADIO); trueButton.setText("true"); final Button falseButton = new Button(choices, SWT.RADIO); falseButton.setText("false"); /* * Add the "Use Default" check-button. If it's checked, then we showed * the slot's default value in the textBox and un-enabled the textBox. */ final Button checkButton = addDefaultCheckbox(group, isSlotSet); final SelectionListener select = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { boolean value = slotValue; boolean useDefault = checkButton.getSelection(); if (useDefault) { value = defaultValue; } trueButton.setEnabled(!useDefault); falseButton.setEnabled(!useDefault); trueButton.setSelection(value); falseButton.setSelection(!value); }}; checkButton.addSelectionListener(select); select.widgetSelected(null); /* * Register a validator for this field. */ registerField(new Validator() { /* any Boolean value is OK - never any errors given */ @Override String getValidationError() { return null; } /* If this field has changed, schedule the undo/redo operation */ @Override void scheduleChange(MultiUndoOp multiOp) { boolean newExists = !checkButton.getSelection(); Boolean newValue = newExists ? Boolean.valueOf(trueButton.getSelection()) : slotValue; if ((newExists != isSlotSet) || !(slotValue.equals(newValue))) { SlotUndoOp op = new SlotUndoOp(buildStore, ISlotTypes.SLOT_OWNER_PACKAGE); op.recordChangeSlotValue(subPkgId, details.slotId, isSlotSet, slotValue, newExists, newValue); multiOp.add(op); } } /* restore default values */ @Override void restoreDefaults() { checkButton.setSelection(!isSlotSet); select.widgetSelected(null); } }); } /*-------------------------------------------------------------------------------------*/ /** * Create the Controls necessary for editing a File or Directory-typed slot. * * @param group The parent group panel to add Control into. * @param details The slot's details. * @param isSlotSet True if the slot currently has a value set, or false if the default * value would be used. * @param defaultValue The default value for the slot. * @param slotValue The slot's current value (or null if !isSlotSet). * @param isFile True if we're modifying a file field, or false for a directory field. */ private void createFileDirEditor(Group group, final SlotDetails details, final boolean isSlotSet, Integer defaultValue, final Integer slotValue, final boolean isFile) { /* * Create the Control that displays the current file name. We use a non-editable Text box, * to make it look consistent with other slots, but without letting the user edit it directly. */ final Text pathLabel = new Text(group, SWT.NONE); pathLabel.setEditable(false); pathLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); /* * Next, show the "Browse" button and the "Use default" check-box, right next to each other. */ Composite pathSelector = new Composite(group, SWT.NONE); pathSelector.setLayoutData(new GridData(SWT.FILL, SWT.None, true, false)); GridLayout layout = new GridLayout(2, false); layout.marginHeight = layout.verticalSpacing = layout.horizontalSpacing = layout.marginWidth = 0; pathSelector.setLayout(layout); final Button browseButton = new Button(pathSelector, SWT.PUSH); browseButton.setText("Browse"); final Button checkButton = addDefaultCheckbox(pathSelector, isSlotSet); /* * Associate actions with the "use default" check-box to appropriately * enable/disable the "Browse" button. */ final SelectionListener select = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { boolean useDefault = checkButton.getSelection(); setPathLabel(pathLabel, useDefault ? null : slotValue); browseButton.setEnabled(!useDefault); } }; checkButton.addSelectionListener(select); select.widgetSelected(null); /* * Define the behaviour of the "Browse" button. This will open a file-selection * dialog and update the path accordingly. */ browseButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { VFSTreeSelectionDialog dialog = new VFSTreeSelectionDialog(getShell(), buildStore, "Please select the new path for this slot", !isFile, isFile); dialog.setAllowMultiple(false); int status = dialog.open(); if (status == VFSTreeSelectionDialog.OK) { Object selectedPath = dialog.getFirstResult(); if (selectedPath instanceof UIInteger) { int pathId = ((UIInteger)selectedPath).getId(); setPathLabel(pathLabel, Integer.valueOf(pathId)); } } } }); /* * Register a validator for this field. */ registerField(new Validator() { /* any file value is OK - never any errors given */ @Override String getValidationError() { return null; } /* If this field has changed, schedule the undo/redo operation */ @Override void scheduleChange(MultiUndoOp multiOp) { boolean newExists = !checkButton.getSelection(); int pathId = fileMgr.getPath(pathLabel.getText()); if (pathId >= 0) { Integer newValue = newExists ? pathId : null; if ((newExists != isSlotSet) || !(slotValue.equals(newValue))) { SlotUndoOp op = new SlotUndoOp(buildStore, ISlotTypes.SLOT_OWNER_PACKAGE); op.recordChangeSlotValue(subPkgId, details.slotId, isSlotSet, slotValue, newExists, newValue); multiOp.add(op); } } } /* restore default values */ @Override void restoreDefaults() { checkButton.setSelection(!isSlotSet); select.widgetSelected(null); } }); } /*-------------------------------------------------------------------------------------*/ /** * Helper method for displaying a "Use Default Value" checkbox. * * @param composite The Composite to add the checkbox to. * @param isSlotSet True if the slot currentl has an explicit value (i.e. does not default) * @return The Button control, that can be queried for its status. */ private Button addDefaultCheckbox(Composite composite, boolean isSlotSet) { /* Create the checkbox */ Button checkButton = new Button(composite, SWT.CHECK); checkButton.setText("Use Default Value"); checkButton.setSelection(!isSlotSet); return checkButton; } /*-------------------------------------------------------------------------------------*/ /** * Validate all of the fields that are being displayed. Whenever a control is modified, * this method is called to validate all fields (not just the field that was modified). * The result is that if any fields contain invalid values, an error message will be * displayed. */ private void validateFields() { for (Validator validator : validators) { String error = validator.getValidationError(); if (error != null) { setMessage(error, ERROR); setValid(false); return; } } setMessage(null); setValid(true); } /*-------------------------------------------------------------------------------------*/ /** * Register a field validator method. Each field editor must register an object that * provides the ability to validate the field, as well as to schedule the field's value * change as an undo/redo operation, and perhaps other things. * * @param validator A Validator object for the field editor. */ private void registerField(Validator validator) { validators.add(validator); } /*-------------------------------------------------------------------------------------*/ /** * Helper method for translating a pathId into a user-facing string, and then setting * the text of a control to that string value. * * @param pathLabel The Control that we'll setting the text for. * @param pathId An Integer containing the pathID (or null). */ private void setPathLabel(Text pathLabel, Integer pathId) { if (pathId == null) { pathLabel.setText("<undefined>"); } else { String pathName = fileMgr.getPathName(pathId); if (pathName == null) { pathLabel.setText("<invalid>"); } else { pathLabel.setText(pathName); } } } /*-------------------------------------------------------------------------------------*/ }