/******************************************************************************* * 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.packages.properties; import java.util.ArrayList; import java.util.Arrays; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.swt.SWT; 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.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.List; import org.eclipse.swt.widgets.Listener; import com.buildml.eclipse.bobj.UIPackage; import com.buildml.eclipse.utils.AlertDialog; 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.SlotDefinitionDialog; 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.model.undo.MultiUndoOp; import com.buildml.model.undo.SlotUndoOp; import com.buildml.utils.errors.ErrorCode; /** * An Eclipse "property" page that allows viewing/editing of a packages slot information. * * Objects of this class are referenced in the plugin.xml file and are dynamically * created when the properties dialog is opened for a UIPackage object. * * @author Peter Smith <psmith@arapiki.com> */ public class PackagePropertyPage extends BmlPropertyPage { /*=====================================================================================* * NESTED CLASSES *=====================================================================================*/ /** * A structure for holding information about a List and it's three Buttons */ private class ListBoxControls { List list; Button newButton; Button editButton; Button deleteButton; } /*=====================================================================================* * CONSTANTS *=====================================================================================*/ /** Value for a List box that contains no slots */ private static final String NO_SLOTS = "--- none defined ---"; /*=====================================================================================* * FIELDS/TYPES *=====================================================================================*/ /** Our IBuildStore */ private IBuildStore buildStore; /** The BuildStore's package manager */ private IPackageMgr pkgMgr; /** ID of the package that these slots belong to */ private int pkgId; /** The original slot information, before any changes are made */ private ArrayList<SlotDetails> originalSlots; /** The slot information as changes are being made */ private ArrayList<SlotDetails> currentSlots; /** The Controls showing the output slots */ private ListBoxControls outputSlotControls; /** The Controls showing the parameters slots */ private ListBoxControls parameterSlotControls; /** The Controls showing the local slots */ private ListBoxControls localSlotControls; /*=====================================================================================* * CONSTRUCTORS *=====================================================================================*/ /** * Create a new SubPackagePropertyPage() object. */ public PackagePropertyPage() { buildStore = EclipsePartUtils.getActiveBuildStore(); pkgMgr = buildStore.getPackageMgr(); } /*=====================================================================================* * 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. */ @Override public boolean performOk() { /* * Compare currentSlots with originalSlot to see what has changed. We maintain two * pointers (originalIndex, currentIndex) as we incrementally progress through the two lists * and look for changes. * Cases: * 1) If currentIndex.slotId == -1, then we have a newly added slot. * 2) If originalIndex.slotId != currentIndex.slotId, then originalIndex.slot must be removed. * 3) else, if currentIndex.* is different from originalIndex.*, the slot has changed. * Note that this assumes that slotIDs are returned in numeric order (guaranteed by getSlots()), * yet that newly-added slots (with slotID == -1) Are at the end of the currentSlots list. */ MultiUndoOp multiOp = new MultiUndoOp(); boolean changeMade = false; int originalIndex = 0; int originalMax = originalSlots.size(); int currentIndex = 0; int currentMax = currentSlots.size(); while ((originalIndex != originalMax) || (currentIndex != currentMax)) { SlotDetails originalDetails = (originalIndex < originalMax) ? originalSlots.get(originalIndex) : null; SlotDetails currentDetails = (currentIndex < currentMax) ? currentSlots.get(currentIndex) : null; /* we've hit a newly-added slot */ if ((currentDetails != null) && (currentDetails.slotId == -1)) { int newSlotId = pkgMgr.newSlot(pkgId, currentDetails.slotName, currentDetails.slotDescr, currentDetails.slotType, currentDetails.slotPos, currentDetails.slotCard, currentDetails.defaultValue, currentDetails.enumValues); if (newSlotId < 0) { throw new FatalError("newSlot failed, even after we validated all parameters"); } SlotUndoOp op = new SlotUndoOp(buildStore, ISlotTypes.SLOT_OWNER_PACKAGE); op.recordNewSlot(newSlotId); multiOp.add(op); changeMade = true; currentIndex++; } /* we've found a deleted slot */ else if ((currentDetails == null) || (currentDetails.slotId != originalDetails.slotId)) { /* try to delete the slot, reporting errors that might occur */ int result = pkgMgr.trashSlot(originalDetails.slotId); if (result == ErrorCode.CANT_REMOVE) { AlertDialog.displayErrorDialog("Failed to delete slot", "The slot " + originalDetails.slotName + " can't be removed, since there are " + "one or more sub-packagse defined that override the default value. Please remove those " + "sub-package instances before trying to delete the slot."); return false; } /* all good, now schedule an undo/redo operation */ else { SlotUndoOp op = new SlotUndoOp(buildStore, ISlotTypes.SLOT_OWNER_PACKAGE); op.recordRemoveSlot(originalDetails.slotId); multiOp.add(op); changeMade = true; originalIndex++; } } /* if any of the changeable fields have changed, we issue a "change" operation */ else { boolean changed = !originalDetails.slotName.equals(currentDetails.slotName); changed = changed || (!originalDetails.slotDescr.equals(currentDetails.slotDescr)); changed = changed || (originalDetails.slotCard != currentDetails.slotCard); changed = changed || ((originalDetails.defaultValue != null) && (!originalDetails.defaultValue.equals(currentDetails.defaultValue))); if (changed) { /* try to change the slot details - this shouldn't fail because input are validated */ if (pkgMgr.changeSlot(currentDetails) != ErrorCode.OK) { throw new FatalError("Error returned from pkgMgr.changeSlot(), even when fields were validated"); } /* record the change for undo/redo purposes */ SlotUndoOp op = new SlotUndoOp(buildStore, ISlotTypes.SLOT_OWNER_PACKAGE); op.recordChangeSlot(originalDetails, currentDetails); multiOp.add(op); changeMade = true; } originalIndex++; currentIndex++; } } if (changeMade) { new UndoOpAdapter("Edit Slots", multiOp).record(); } return super.performOk(); } /*-------------------------------------------------------------------------------------*/ /** * The "restore default" button has been pressed, so return everything back to its * original state. */ @Override protected void performDefaults() { /* restore the currentSlots to the values in originalSlots */ currentSlots = duplicateSlotList(originalSlots); /* refresh all the list boxes */ populateSlotList(outputSlotControls, ISlotTypes.SLOT_POS_OUTPUT); populateSlotList(parameterSlotControls, ISlotTypes.SLOT_POS_PARAMETER); populateSlotList(localSlotControls, ISlotTypes.SLOT_POS_LOCAL); super.performDefaults(); } /*=====================================================================================* * PROTECTED METHODS *=====================================================================================*/ /** * Create the widgets that appear within the properties dialog box. */ @Override protected Control createContents(Composite parent) { /* * Determine the numeric ID of the package. We have the following choices * for getElement(): * 1) We're passed a UIPackage, which is what we want. * 2) We're passed something that adapts to UIPackage. * 3) We're passed a Graphiti EditPart that converts to a UIPackage. * Otherwise give up completely and display an error. */ UIPackage pkg = null; if (getElement() instanceof UIPackage) { pkg = (UIPackage) getElement(); } else if (getElement() instanceof IAdaptable) { IAdaptable adapter = (IAdaptable)getElement(); pkg = (UIPackage) adapter.getAdapter(UIPackage.class); } if (pkg == null) { pkg = (UIPackage) GraphitiUtils.getBusinessObjectFromElement(getElement(), UIPackage.class); } /* couldn't identify the UIPackage - give an error */ if (pkg == null) { setErrorMessage("Selected element is not a recognized package"); return null; } /* * We're good, and we have a UIPackage, so display the package information. */ pkgId = pkg.getId(); String pkgName = pkgMgr.getName(pkgId); setTitle("Package Slots: " + pkgName); /* * Fetch the slot information, and make a copy of it so we can restore it * later if necessary. We'll use this information as our "database", rather * than actually modifying the buildstore. This should only happen when * the final "OK" button is pressed. */ originalSlots = new ArrayList<SlotDetails>(); SlotDetails details[] = pkgMgr.getSlots(pkgId, ISlotTypes.SLOT_POS_ANY); for (int i = 0; i < details.length; i++) { originalSlots.add(details[i]); } currentSlots = duplicateSlotList(originalSlots); /* * Create a panel in which all sub-widgets are added. The first (of 2) * columns will content the "list" of slots defined for this package. * The second (of 2) columns contain buttons for performing actions * on those slots. */ Composite panel = new Composite(parent, SWT.NONE); GridLayout layout = new GridLayout(); layout.marginHeight = 0; layout.marginWidth = 0; layout.numColumns = 2; panel.setLayout(layout); /* display the lists of slots (output, parameter and local) */ outputSlotControls = createSlotList(panel, "Output Slots:", ISlotTypes.SLOT_POS_OUTPUT); parameterSlotControls = createSlotList(panel, "Parameter Slots:", ISlotTypes.SLOT_POS_PARAMETER); localSlotControls = createSlotList(panel, "Local Slots:", ISlotTypes.SLOT_POS_LOCAL); return panel; } /*=====================================================================================* * PRIVATE METHODS *=====================================================================================*/ /** * Make a deep copy of the slots list. We use this to create a "backup" of the slot * information in case we "restore defaults". * * @param originalSlots The original list to be copied. * @return A duplicate of the original list. */ private ArrayList<SlotDetails> duplicateSlotList(ArrayList<SlotDetails> originalSlots) { ArrayList<SlotDetails> newList = new ArrayList<SlotDetails>(); for (SlotDetails slot : originalSlots) { newList.add(new SlotDetails(slot)); } return newList; } /*-------------------------------------------------------------------------------------*/ /** * Create a list box and appropriate buttons, to allow a specific slot type to be * manipulated. * * @param panel The Composite that the list box (and buttons) should be placed in. * @param title The textual title to be shown above the list box. * @param slotPos The type of slot to manipulate (e.g. ISlotType.SLOT_POS_OUTPUT). * * @return The List controls (list and buttons). */ private ListBoxControls createSlotList(Composite panel, String title, final int slotPos) { final ListBoxControls controls = new ListBoxControls(); /* * The title for this section. */ Label titleLabel = new Label(panel, SWT.None); titleLabel.setText(title); GridData gd = new GridData(); gd.horizontalSpan = 2; titleLabel.setLayoutData(gd); /* * The first column - the list of slots in the package. */ controls.list = new List(panel, SWT.SINGLE | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL); controls.list.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); /* * The second column - buttons that we can press to modify the slots. */ Composite buttonPanel = new Composite(panel, SWT.NONE); buttonPanel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); RowLayout buttonPanelLayout = new RowLayout(SWT.VERTICAL); buttonPanelLayout.fill = true; buttonPanelLayout.marginLeft = buttonPanelLayout.marginRight = 10; buttonPanelLayout.spacing = 10; buttonPanel.setLayout(buttonPanelLayout); /* new button - for adding new slots to this package */ controls.newButton = new Button(buttonPanel, SWT.NONE); controls.newButton.setText("New Slot"); controls.newButton.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event event) { performNewOperation(controls, slotPos); } }); /* edit button - for changing a slot's information */ controls.editButton = new Button(buttonPanel, SWT.NONE); controls.editButton.setText("Edit"); controls.editButton.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event event) { performEditOperation(controls, slotPos); } }); /* delete button - for removing a slot from this package */ controls.deleteButton = new Button(buttonPanel, SWT.NONE); controls.deleteButton.setText("Delete"); controls.deleteButton.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event event) { performDeleteOperation(controls, slotPos); } }); /* * When items in the list box are selected/deselected, we need to enable/disable * the buttons accordingly. By default, only the "new" button is enabled. */ controls.newButton.setEnabled(true); controls.editButton.setEnabled(false); controls.deleteButton.setEnabled(false); controls.list.addSelectionListener(new SelectionAdapter() { /* select the name evaluates the button-enabled statuses */ @Override public void widgetSelected(SelectionEvent e) { String selectedNames[] = controls.list.getSelection(); if ((selectedNames.length == 1) && (selectedNames[0].equals(NO_SLOTS))) { return; } controls.editButton.setEnabled(true); controls.deleteButton.setEnabled(true); } /* double-click performs "edit" */ @Override public void widgetDefaultSelected(SelectionEvent e) { performEditOperation(controls, slotPos); } }); /* populate the List control with all the slots */ populateSlotList(controls, slotPos); return controls; } /*-------------------------------------------------------------------------------------*/ /** * Populate the specified List Control with the names of the slots (from the "pkgId" * package). * * @param controls The List Controls to populate. * @param slotPos The type of slot (e.g. ISlotType.SLOT_POS_OUTPUT). */ private void populateSlotList(ListBoxControls controls, int slotPos) { /* filter which of the slots we want to see (based on slotPos) */ ArrayList<String> names = new ArrayList<String>(); for (SlotDetails slot: currentSlots) { if (slot.slotPos == slotPos) { names.add(slot.slotName); } } String sortedNames[] = names.toArray(new String[0]); Arrays.sort(sortedNames); /* * Now add the alphabetically sorted names to the list. For empty lists, * show the NO_SLOTS string. */ controls.list.removeAll(); controls.editButton.setEnabled(false); controls.deleteButton.setEnabled(false); /* * Set the button enabled status, as necessary. */ if (sortedNames.length == 0) { controls.list.add(NO_SLOTS); } else { for (int i = 0; i < sortedNames.length; i++) { controls.list.add(sortedNames[i]); } } } /*-------------------------------------------------------------------------------------*/ /** * Return the index of the currently-selected slot within the currentSlots list, or * -1 if nothing is selected (or if the NO_SLOTS line is selected). * * @param slotList The List box to retrieve the selection from. * @return Index of the selected slot within the currentSlots list (or -1 if not found). */ private int getSelectedSlotDetails(List slotList) { /* What is the selected text from the list box? */ String selectedNames[] = slotList.getSelection(); if (selectedNames.length != 1) { return -1; } String slotName = selectedNames[0]; if (slotName.equals(NO_SLOTS)) { return -1; } /* compute the index of this text from the list */ int i = 0; for (SlotDetails details : currentSlots) { if (details.slotName.equals(slotName)) { return i; } i++; } return i; } /*-------------------------------------------------------------------------------------*/ /** * The user has pressed the "Delete" button, so proceed to remove the slot. * * @param controls The Controls that contain the list of slots. * @param slotPos The position of the slot (e.g. SLOT_POS_OUTPUT). */ private void performDeleteOperation(ListBoxControls controls, int slotPos) { /* fetch the slot name */ int index = getSelectedSlotDetails(controls.list); if (index != -1) { currentSlots.remove(index); populateSlotList(controls, slotPos); controls.list.deselectAll(); } } /*-------------------------------------------------------------------------------------*/ /** * The user has pressed the "Edit" button, so proceed to edit the slot details. * * @param controls The Controls that contain the list of slots. * @param slotPos The position of the slot (e.g. SLOT_POS_OUTPUT). */ private void performEditOperation(ListBoxControls controls, int slotPos) { /* get the details for the currently selected slot */ int index = getSelectedSlotDetails(controls.list); if (index == -1) { return; } SlotDetails selectedDetails = currentSlots.get(index); /* create a dialog so the user can edit these defaults */ SlotDefinitionDialog dialog = new SlotDefinitionDialog(buildStore, false, selectedDetails, currentSlots); int status = dialog.open(); /* on OK, insert the new slot details back into our active set of slots */ if (status == SlotDefinitionDialog.OK) { SlotDetails editedDetails = dialog.getSlotDetails(); currentSlots.set(index, editedDetails); populateSlotList(controls, slotPos); } } /*-------------------------------------------------------------------------------------*/ /** * The user has pressed the "New Slot" button, so open a dialog that allows them to * define the new slot. * * @param controls The Controls that display the list of slots. * @param slotPos The position of the slot (e.g. SLOT_POS_OUTPUT). */ private void performNewOperation(ListBoxControls controls, int slotPos) { /* create suitable default slot details - the default depends on slotPos */ SlotDetails defaults; switch (slotPos) { case ISlotTypes.SLOT_POS_INPUT: case ISlotTypes.SLOT_POS_OUTPUT: defaults = new SlotDetails( -1, ISlotTypes.SLOT_OWNER_PACKAGE, pkgId, "Enter a new slot name", "", ISlotTypes.SLOT_TYPE_FILEGROUP, slotPos, ISlotTypes.SLOT_CARD_REQUIRED, null, null); break; case ISlotTypes.SLOT_POS_PARAMETER: case ISlotTypes.SLOT_POS_LOCAL: defaults = new SlotDetails( -1, ISlotTypes.SLOT_OWNER_PACKAGE, pkgId, "Enter a new slot name", "", ISlotTypes.SLOT_TYPE_TEXT, slotPos, ISlotTypes.SLOT_CARD_OPTIONAL, null, null); break; default: throw new FatalError("Unrecognized slotPos value"); } /* create a dialog so the user can edit these defaults */ SlotDefinitionDialog dialog = new SlotDefinitionDialog(buildStore, true, defaults, currentSlots); int status = dialog.open(); /* on OK, add the new slot details to our active set of slots */ if (status == SlotDefinitionDialog.OK) { SlotDetails newDetails = dialog.getSlotDetails(); currentSlots.add(newDetails); populateSlotList(controls, slotPos); } } /*-------------------------------------------------------------------------------------*/ }