/*******************************************************************************
* Copyright (c) 2013 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 java.util.regex.PatternSyntaxException;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FillLayout;
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.Label;
import org.eclipse.swt.widgets.List;
import com.buildml.eclipse.bobj.UIConnection;
import com.buildml.eclipse.bobj.UIFileActionConnection;
import com.buildml.eclipse.bobj.UIMergeFileGroupConnection;
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.model.IBuildStore;
import com.buildml.model.IFileGroupMgr;
import com.buildml.model.undo.FileGroupUndoOp;
import com.buildml.model.undo.MultiUndoOp;
import com.buildml.utils.regex.BmlRegex;
import com.buildml.utils.regex.RegexChain;
/**
* An Eclipse "property" page that allows viewing/editing of a connection's properties.
* Since connections are generally quite simple, this properties page focuses mostly
* on the filter file group that can decorate a connection.
*
* Objects of this class are referenced in the plugin.xml file and are dynamically
* created when the properties dialog is opened for a UIAction object.
*
* @author Peter Smith <psmith@arapiki.com>
*/
public class ConnectionPropertyPage extends BmlPropertyPage {
/*=====================================================================================*
* FIELDS/TYPES
*=====================================================================================*/
/** ID of the input group that we're filtering */
private int inputFileGroupId;
/** ID of the filter group itself (contains the patterns) */
private int filterFileGroupId;
/** The IBuildStore we query information from */
private IBuildStore buildStore = null;
/** The IFileGroupMgr we query information from */
private IFileGroupMgr fileGroupMgr = null;
/** The Control for the left panel's list box */
private List leftListBox;
/** The Control for the middle panel's list box */
private List middleListBox;
/** The Control for the right panel's list box */
private List rightListBox;
/** The various buttons */
@SuppressWarnings("javadoc")
private Button includeButton, excludeButton, addButton, editButton, removeButton,
moveUpButton, moveDownButton;
/** The list of patterns shown in the middle list box (this is what we're editing) */
private ArrayList<String> filterFilePaths;
/** The initial list of patterns when we opened the properties box (for restoring) */
private ArrayList<String> initialFilterFilePaths;
/** The UIConnection object that our filter is applied to */
private UIConnection connection;
/*=====================================================================================*
* CONSTRUCTORS
*=====================================================================================*/
/**
* Create a new ConnectionPropertyPage() object.
*/
public ConnectionPropertyPage() {
/* nothing */
}
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/**
* Because of the size of this property page, we make it twice the width as normal.
*/
@Override
public Point computeSize() {
Point standardSize = super.computeSize();
return new Point(standardSize.x * 2, standardSize.y);
}
/*-------------------------------------------------------------------------------------*/
/**
* 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() {
/*
* when OK is pressed (assuming there's a filter), we need to create an undo/redo
* operation to initiate the changes. This code is complicated by the fact that
* the connection object might have an existing undo/redo operation attached to it.
* If so, extend this operation. If not, create a new operation.
*/
if (connection.hasFilter()) {
/* create an undo/redo operation that will invoke the underlying database changes */
FileGroupUndoOp op = new FileGroupUndoOp(buildStore, filterFileGroupId);
op.recordMembershipChange(initialFilterFilePaths, filterFilePaths);
/* If this properties dialog was invoked as part of the "add filter" operation? */
MultiUndoOp creationMultiOp = connection.getUndoRedoOperation();
if (creationMultiOp != null) {
creationMultiOp.add(op);
new UndoOpAdapter("Add Filter", creationMultiOp).invoke();
}
/* no, this is a standalone "properties" command, invoked via the "Properties" menu option */
else {
new UndoOpAdapter("Modify Filter", op).invoke();
}
}
return super.performOk();
}
/*-------------------------------------------------------------------------------------*/
/**
* The cancel button was pressed. Reset the undo/redo multiOp to indicate to the
* "add filter" command (HandlerNewFilter) that the user pressed cancel.
*/
@Override
public boolean performCancel() {
if (connection.hasFilter()) {
connection.setUndoRedoOperation(null);
}
return super.performCancel();
}
/*-------------------------------------------------------------------------------------*/
/**
* The "restore default" button has been pressed, so return the filter list back to
* its original state.
*/
@Override
protected void performDefaults() {
if (connection.hasFilter()) {
/* restore back to the original list */
filterFilePaths.clear();
filterFilePaths.addAll(initialFilterFilePaths);
/* refresh the view */
refreshMiddleList();
setMiddlePanelButtons();
refreshRightList();
}
super.performDefaults();
}
/*=====================================================================================*
* PROTECTED METHODS
*=====================================================================================*/
/**
* Create the widgets that appear within the properties dialog box.
*/
@Override
protected Control createContents(Composite parent) {
connection = (UIConnection)GraphitiUtils.getBusinessObjectFromElement(getElement(), UIConnection.class);
if (connection == null) {
return null;
}
setTitle("Connection Properties:");
/* if there's no connection set, just display an informational message and exit */
if (!connection.hasFilter()) {
return displayNoFilterSet(parent, connection);
}
/* what are we displaying? What filter group is filter what input group? */
filterFileGroupId = connection.getFilterGroupId();
if (connection instanceof UIFileActionConnection) {
inputFileGroupId = ((UIFileActionConnection)connection).getFileGroupId();
} else {
inputFileGroupId = ((UIMergeFileGroupConnection)connection).getSourceFileGroupId();
}
buildStore = EclipsePartUtils.getActiveBuildStore();
fileGroupMgr = buildStore.getFileGroupMgr();
/* fetch the initial list of filters */
getInitialPatternList();
/*
* We have three columns to display. The left column is the input file group (that
* we're filtering). The middle column contains the filter patterns that we're editing.
* The right column contains the output from the filter (a subset of the left column).
*/
Composite panel = new Composite(parent, SWT.NONE);
panel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
GridLayout layout = new GridLayout(3, true);
layout.marginHeight = 0;
layout.marginWidth = 0;
panel.setLayout(layout);
Control leftPanel = createLeftPanel(panel);
leftPanel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
Control middlePanel = createMiddlePanel(panel);
middlePanel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
Control rightPanel = createRightPanel(panel);
rightPanel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
return panel;
}
/*=====================================================================================*
* PRIVATE METHODS
*=====================================================================================*/
/**
* Compute the initial list of patterns. We need to have this in our local data
* structures so that we can modify it as much as we like, without writing it back to
* the BuildStore (which will trigger lots of notifications). Once OK is pressed, that's
* when it's written back to the database.
*/
private void getInitialPatternList() {
/* fetch the initial list of patterns */
String initialPatterns[] = fileGroupMgr.getPathStrings(filterFileGroupId);
filterFilePaths = new ArrayList<String>();
filterFilePaths.addAll(Arrays.asList(initialPatterns));
/* make a copy, if we need to restore later */
initialFilterFilePaths = new ArrayList<String>();
initialFilterFilePaths.addAll(Arrays.asList(initialPatterns));
/*
* In the case where the filter was just created, there will be 0 patterns.
* We must now add a default pattern.
*/
if (initialPatterns.length == 0) {
filterFilePaths.add("ia:**");
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Create the left-side panel. This panel features a list box showing the input files
* (before the filter). The user can't change this list, but they can choose to "include"
* or "exclude" these files in the pattern (middle) panel.
*
* @param parent The parent control to populate with widgets.
* @return The top-level control we created.
*/
private Control createLeftPanel(Composite parent) {
/*
* The left panel has a single column, with intro text at the top,
* the list box (of input files) in the middle, and the buttons
* at the bottom.
*/
Composite subPanel = new Composite(parent, SWT.None);
GridLayout layout = new GridLayout(1, true);
layout.marginHeight = 10;
layout.marginWidth = 10;
subPanel.setLayout(layout);
/*
* One line of introductory text.
*/
Label headerText = new Label(subPanel, SWT.None);
headerText.setLayoutData(new GridData(SWT.CENTER, SWT.FILL, true, false));
headerText.setText("Input Files (Before Filtering):");
/*
* Create and populate list box (it never changes beyond this point).
*/
leftListBox = new List(subPanel, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL);
leftListBox.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
String [] inputFilePaths = fileGroupMgr.getExpandedGroupFiles(inputFileGroupId);
if (inputFilePaths != null) {
for (int i = 0; i < inputFilePaths.length; i++) {
leftListBox.add(inputFilePaths[i]);
}
}
/* If an item is selected, we may (or may not) need to enable buttons */
leftListBox.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
setLeftPanelButtons();
middleListBox.deselectAll();
}
});
/*
* Create the "include" and "exclude" buttons in a row by themselves.
*/
Composite buttonRow = new Composite(subPanel, SWT.None);
buttonRow.setLayout(new FillLayout());
buttonRow.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
includeButton = new Button(buttonRow, SWT.PUSH);
includeButton.setText("Include File(s)");
includeButton.setEnabled(false);
includeButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
handleIncludeExcludeButton(true);
}
});
excludeButton = new Button(buttonRow, SWT.PUSH);
excludeButton.setText("Exclude File(s)");
excludeButton.setEnabled(false);
excludeButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
handleIncludeExcludeButton(false);
}
});
return subPanel;
}
/*-------------------------------------------------------------------------------------*/
/**
* Create the middle panel. This panel features a list box showing the filter patterns.
* The user can interactively add/remove/modify these patterns. Patterns can be reordered
* (within the list), although doing so won't impact the filter's result.
*
* @param parent The parent control to populate with widgets.
* @return The top-level control we created.
*/
private Control createMiddlePanel(Composite parent) {
/*
* The middle panel has a single column, with intro text at the top,
* the list box (of patterns) in the middle, and the buttons at the bottom.
*/
Composite subPanel = new Composite(parent, SWT.None);
GridLayout layout = new GridLayout(1, true);
layout.marginHeight = 10;
layout.marginWidth = 10;
subPanel.setLayout(layout);
/*
* One line of introductory text.
*/
Label headerText = new Label(subPanel, SWT.None);
headerText.setLayoutData(new GridData(SWT.CENTER, SWT.FILL, true, false));
headerText.setText("Filter Patterns:");
/*
* Create the listbox (which will change often).
*/
middleListBox = new List(subPanel, SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL | SWT.H_SCROLL);
middleListBox.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
/*
* Create the "add", "edit", "remove", "up" and "down" buttons in a row by themselves.
*/
Composite buttonRow = new Composite(subPanel, SWT.None);
buttonRow.setLayout(new FillLayout());
buttonRow.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
addButton = new Button(buttonRow, SWT.PUSH);
addButton.setText("Add");
addButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
handleAddEditButton(true);
}
});
editButton = new Button(buttonRow, SWT.PUSH);
editButton.setText("Edit");
editButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
handleAddEditButton(false);
}
});
removeButton = new Button(buttonRow, SWT.PUSH);
removeButton.setText("Remove");
removeButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
handleRemoveButton();
}
});
moveUpButton = new Button(buttonRow, SWT.PUSH);
moveUpButton.setText("Move Up");
moveUpButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
handleMoveButton(-1);
}
});
moveDownButton = new Button(buttonRow, SWT.PUSH);
moveDownButton.setText("Move Down");
moveDownButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
handleMoveButton(1);
}
});
/* display the content of our listbox and set button states appropriately */
refreshMiddleList();
setMiddlePanelButtons();
/*
* Handle selection of items in the list box. All buttons are initially disabled
* until a list box item is selected.
*/
middleListBox.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
setMiddlePanelButtons();
leftListBox.deselectAll();
}
});
return subPanel;
}
/*-------------------------------------------------------------------------------------*/
/**
* Create the right panel. This panel features a list box showing the output of the filter.
* The user can no interact with this panel at all, except for scrolling.
*
* @param parent The parent control to populate with widgets.
* @return The top-level control we created.
*/
private Control createRightPanel(Composite parent) {
/*
* The right panel has a single column, with intro text at the top,
* the list box (of patterns) in the middle, and a filler at the bottom
* so that all three list boxes (left, middle, right) will have the same height.
*/
Composite subPanel = new Composite(parent, SWT.None);
GridLayout layout = new GridLayout(1, true);
layout.marginHeight = 10;
layout.marginWidth = 10;
subPanel.setLayout(layout);
/*
* One line of introductory text.
*/
Label headerText = new Label(subPanel, SWT.None);
headerText.setLayoutData(new GridData(SWT.CENTER, SWT.FILL, true, false));
headerText.setText("Output Files (After Filtering):");
/*
* Create and populate the list box control.
*/
rightListBox = new List(subPanel, SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL | SWT.H_SCROLL);
rightListBox.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
refreshRightList();
/*
* Create a filler button - to make all list boxes the same height.
*/
Composite buttonRow = new Composite(subPanel, SWT.None);
buttonRow.setLayout(new FillLayout());
buttonRow.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
Button filler = new Button(buttonRow, SWT.PUSH);
filler.setText("");
filler.setVisible(false);
return subPanel;
}
/*-------------------------------------------------------------------------------------*/
/**
* Display a message informing the user that no filter is set and therefore no editing
* of properties is possible.
*
* @param parent The parent control for our content.
* @param connection The business object we're viewing (UIFileActionConnection etc).
* @return The new controls we created.
*/
private Control displayNoFilterSet(Composite parent, UIConnection connection) {
/* create a panel in which all sub-widgets are added. */
Composite panel = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout();
layout.marginHeight = 0;
layout.marginWidth = 0;
panel.setLayout(layout);
String extraMessage = "You do not currently have a filter attached.";
if ((connection instanceof UIFileActionConnection) &&
((UIFileActionConnection)connection).getDirection() == UIFileActionConnection.OUTPUT_FROM_ACTION) {
extraMessage = "It is not possible to attach a filter on this connection (output from an action).";
}
/* if we don't have a filter, there's nothing we can do */
Label message = new Label(panel, SWT.None);
message.setText("This properties page can be used to view/edit the content of " +
"connection filters. " + extraMessage);
Label filler = new Label(panel, SWT.None);
filler.setLayoutData(new GridData(SWT.None, SWT.None, true, true));
return panel;
}
/*-------------------------------------------------------------------------------------*/
/**
* Refresh the content of the middle list box (with the filter's output). This
* list will need to be redrawn whenever a pattern is added/removed/modified.
*/
private void refreshMiddleList(){
middleListBox.removeAll();
for (int i = 0; i < filterFilePaths.size(); i++) {
String parts[] = filterFilePaths.get(i).split(":");
String prefix = null;
if (parts[0].equals("ia")) {
prefix = "Include";
} else if (parts[0].equals("ea")) {
prefix = "Exclude";
}
if (prefix != null) {
middleListBox.add(prefix + ": " + parts[1]);
}
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Refresh the content of the right-side list box (with the filter's output). This
* list content may change every time the filter's patterns change. It would be
* nice to use the IFileGroupMgr to do this for us, but that triggers all sorts
* of change events that we don't want to see right now.
*/
private void refreshRightList(){
/* compile the filter's file paths into a regular expression chain */
RegexChain chain;
try {
chain = BmlRegex.compileRegexChain(filterFilePaths.toArray(new String[0]));
} catch (PatternSyntaxException ex) {
return;
}
/* fetch the input paths... */
String [] inputFilePaths = fileGroupMgr.getExpandedGroupFiles(inputFileGroupId);
/* Compute the filtered paths */
String filteredPaths[] = BmlRegex.filterRegexChain(inputFilePaths, chain);
/* display them in the list box */
rightListBox.removeAll();
if (filteredPaths != null) {
for (int i = 0; i < filteredPaths.length; i++) {
rightListBox.add(filteredPaths[i]);
}
}
}
/*-------------------------------------------------------------------------------------*/
/**
* The user has selected one or more items in the left panel. We must therefore enable/disable
* the include/exclude buttons as appropriate. We only enable the "include" button if there
* is at least one selection that isn't already in the pattern. Likewise, we only enable the
* "exclude" button if the selected file already has an associated pattern.
*/
private void setLeftPanelButtons() {
boolean includeOK = false, excludeOK = false;
/* given the selection, are we able to include/exclude the selected files? */
String selection[] = leftListBox.getSelection();
for (int i = 0; i < selection.length; i++) {
/* search the patterns to see if the selected file(s) are already there. */
String thisFile = selection[i];
includeOK = (findPattern("ia:" + thisFile) == -1);
excludeOK = (findPattern("ea:" + thisFile) == -1);
}
includeButton.setEnabled(includeOK);
excludeButton.setEnabled(excludeOK);
}
/*-------------------------------------------------------------------------------------*/
/**
* The middle panel has somehow changed, and we need to figure out the appropriate state
* for all the buttons.
*/
private void setMiddlePanelButtons() {
int index = middleListBox.getSelectionIndex();
if (index == -1) {
editButton.setEnabled(false);
removeButton.setEnabled(false);
moveUpButton.setEnabled(false);
moveDownButton.setEnabled(false);
return;
}
editButton.setEnabled(true);
removeButton.setEnabled(true);
int filterSize = filterFilePaths.size();
moveUpButton.setEnabled((filterSize > 1) && (index != 0));
moveDownButton.setEnabled((filterSize > 1) && (index != filterSize-1));
}
/*-------------------------------------------------------------------------------------*/
/**
* The user has pressed the "include" button or the "exclude" button. If we're including,
* we'll remove any explicit excludes and then add our new include pattern. The opposite
* is true for excluding.
* @param doInclude True if the user pressed "include", else false.
*/
protected void handleIncludeExcludeButton(boolean doInclude) {
String selection[] = leftListBox.getSelection();
for (int i = 0; i < selection.length; i++) {
String thisPattern = selection[i];
/* first, check if there's already a pattern that we need to remove */
int removeIndex = findPattern((doInclude ? "ea" : "ia") + ":" + thisPattern);
if (removeIndex != -1){
filterFilePaths.remove(removeIndex);
}
/* if it doesn't already exist, add the new pattern at the end of the filter group */
String patternToAdd = (doInclude ? "ia" : "ea") + ":" + thisPattern;
int addIndex = findPattern(patternToAdd);
if (addIndex == -1) {
filterFilePaths.add(patternToAdd);
}
}
/* update the view, setting buttons to appropriate states */
refreshMiddleList();
setMiddlePanelButtons();
refreshRightList();
/* update button appropriately */
includeButton.setEnabled(!doInclude);
excludeButton.setEnabled(doInclude);
}
/*-------------------------------------------------------------------------------------*/
/**
* Handle the "move up" and "move down" buttons.
*
* @param direction The direction of the move (-1 for up, 1 for down).
*/
protected void handleMoveButton(int direction) {
int index = middleListBox.getSelectionIndex();
if (index == -1) {
return;
}
if ((index + direction < 0) || (index + direction >= filterFilePaths.size())) {
return;
}
String value = filterFilePaths.get(index);
filterFilePaths.remove(index);
filterFilePaths.add(index + direction, value);
refreshMiddleList();
middleListBox.setSelection(index + direction);
setMiddlePanelButtons();
}
/*-------------------------------------------------------------------------------------*/
/**
* Handle the "remove" button. Note that we can never truly be empty, since the "ia:**"
* pattern will always be there if nothing else is.
*/
protected void handleRemoveButton() {
int index = middleListBox.getSelectionIndex();
if (index != -1) {
filterFilePaths.remove(index);
if (filterFilePaths.isEmpty()) {
filterFilePaths.add("ia:**");
}
/* refresh to see the new list, but keep the "next" item selected */
refreshMiddleList();
middleListBox.setSelection((index < filterFilePaths.size() ? index : index - 1));
setMiddlePanelButtons();
refreshRightList();
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Handle the "add" and "edit" buttons.
*
* @param createNew True if we should create a new pattern, else false to edit currently
* selected item.
*/
protected void handleAddEditButton(boolean createNew) {
/* create a new Dialog for editing the filter pattern */
ConnectionPatternDialog dialog = new ConnectionPatternDialog(createNew);
/* If "edit" is pressed, fetch the currently selected pattern */
int index = -1;
if (!createNew){
index = middleListBox.getSelectionIndex();
if (index == -1) {
return;
}
String pattern = filterFilePaths.get(index);
dialog.setPattern(pattern);
}
/* open the dialog and wait for either OK or Cancel to be pressed */
int status = dialog.open();
if (status == ConnectionPatternDialog.OK){
/*
* OK was pressed, either add the new pattern to the end of the group, or replace
* the currently selected pattern.
*/
String newPattern = dialog.getPattern();
if (createNew) {
filterFilePaths.add(newPattern);
} else {
filterFilePaths.set(index, newPattern);
}
refreshMiddleList();
setMiddlePanelButtons();
refreshRightList();
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Determine whether a pattern is already in the filter group.
*
* @param patternToMatch The pattern to look for.
* @return The 0-based index of the pattern, or -1 if not found.
*/
private int findPattern(String patternToMatch) {
int size = filterFilePaths.size();
for (int i = 0; i < size; i++) {
if (patternToMatch.equals(filterFilePaths.get(i))) {
return i;
}
}
return -1;
}
/*-------------------------------------------------------------------------------------*/
}