package net.sourceforge.pmd.eclipse.ui.preferences.editors;
import java.util.ArrayList;
import java.util.List;
import net.sourceforge.pmd.PropertyDescriptor;
import net.sourceforge.pmd.PropertySource;
import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.eclipse.ui.preferences.br.SizeChangeListener;
import net.sourceforge.pmd.eclipse.ui.preferences.br.ValueChangeListener;
import net.sourceforge.pmd.eclipse.util.Util;
import net.sourceforge.pmd.util.StringUtil;
import org.eclipse.swt.SWT;
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.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
/**
* As a stateless factory it is responsible for building editors that manipulating value collections
* without retaining references to the widgets or values themselves. All necessary references are
* held in the event handlers and passed onto any new handlers created to manage values newly created
* by the user - hence the monster method calls with umpteen arguments.
*
* Concrete subclasses are responsible for instantiating the type-appropriate edit widgets, retrieving
* their values, and updating the rule property. Provided you have widget capable of displaying/editing
* your value type you can use this class as a base to bring up the appropriate widgets for the
* individual values.
*
* The editor is held in a composite divided into three columns. In the collapsed mode, a text field
* displaying the value collection occupies the first two cells with an expand/collapse button in the
* last cell. When the user clicks the button the row beneath is given a label, a type-specific edit
* widget, and a control button for every value in the collection. The last row is empty and serves as
* a place the user can enter additional values. When the user enters a value and clicks the control
* button that row becomes read-only and a new empty row is added to the bottom.
*
* Note inclusion of the size and value changed callbacks used to let the parent composite resize itself
* and update the values in the rule listings respectively.
*
* @author Brian Remedios
*/
public abstract class AbstractMultiValueEditorFactory extends AbstractEditorFactory {
protected static final String delimiter = ",";
private static final int WidgetsPerRow = 3; // numberLabel, valueWidget, +/-button
protected AbstractMultiValueEditorFactory() {
}
protected abstract void configure(Text text, PropertyDescriptor<?> desc, PropertySource source, ValueChangeListener listener);
protected abstract void setValue(Control widget, Object value);
protected abstract void update(PropertySource source, PropertyDescriptor<?> desc, List<Object> newValues);
protected abstract Object addValueIn(Control widget, PropertyDescriptor<?> desc, PropertySource source);
protected abstract Control addWidget(Composite parent, Object value, PropertyDescriptor<?> desc, PropertySource source);
/**
*
* @param parent Composite
* @param desc PropertyDescriptor
* @param rule Rule
* @param listener ValueChangeListener
* @return Control
* @see net.sourceforge.pmd.ui.preferences.br.EditorFactory#newEditorOn(Composite, PropertyDescriptor, Rule, ValueChangeListener, SizeChangeListener)
*/
public Control newEditorOn(final Composite parent, final PropertyDescriptor<?> desc, final PropertySource source, final ValueChangeListener changeListener, final SizeChangeListener sizeListener) {
final Composite panel = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout(3, false);
layout.verticalSpacing = 0;
layout.marginHeight = 0;
layout.marginWidth = 0;
panel.setLayout(layout);
final Text textWidget = new Text(panel, SWT.SINGLE | SWT.BORDER);
final Button butt = new Button(panel, SWT.PUSH);
butt.setText("..."); // TODO use triangle icon & rotate 90deg when clicked
butt.addListener(SWT.Selection, new Listener() {
boolean itemsVisible = false;
List<Control> items = new ArrayList<Control>();
public void handleEvent(Event event) {
if (itemsVisible) {
hideCollection(items);
sizeListener.addedRows(items.size() / -WidgetsPerRow);
} else {
items = openCollection(panel, desc, source, textWidget, changeListener, sizeListener);
sizeListener.addedRows(items.size() / WidgetsPerRow);
}
itemsVisible = !itemsVisible;
textWidget.setEditable(!itemsVisible); // no raw editing when individual items are available
parent.layout();
}
});
GridData data = new GridData(GridData.FILL_HORIZONTAL);
data.horizontalSpan = 2;
textWidget.setLayoutData(data);
panel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
fillWidget(textWidget, desc, source);
configure(textWidget, desc, source, changeListener);
return panel;
}
private void hideCollection(List<Control> controls) {
for (Control control : controls) control.dispose();
}
private void delete(Control number, Control widget, Control button, List<Control> controlList, Object deleteValue, PropertyDescriptor<?> desc, PropertySource source) {
controlList.remove(number); number.dispose();
controlList.remove(widget); widget.dispose();
controlList.remove(button); button.dispose();
renumberLabelsIn(controlList);
Object[] values = (Object[])valueFor(source, desc);
List<Object> newValues = new ArrayList<Object>(values.length - 1);
for (Object value : values) {
if (value.equals(deleteValue)) continue;
newValues.add(value);
}
update(source, desc, newValues);
}
private List<Control> openCollection(final Composite parent, final PropertyDescriptor<?> desc, final PropertySource source, final Text textWidget, final ValueChangeListener changeListener, final SizeChangeListener sizeListener) {
final List<Control> newControls = new ArrayList<Control>();
int i;
Object[] values = (Object[])valueFor(source, desc);
for (i=0; i<values.length; i++) {
final Label number = new Label(parent, SWT.NONE);
number.setText(Integer.toString(i+1));
final Control widget = addWidget(parent, values[i], desc, source);
widget.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
widget.setEnabled(false);
final Button butt = new Button(parent, SWT.PUSH);
butt.setText("-"); // TODO use icon for consistent width
final Object value = values[i];
butt.addListener(SWT.Selection, new Listener() { // remove value handler
public void handleEvent(Event event) {
delete(number, widget, butt, newControls, value, desc, source);
fillWidget(textWidget, desc, source);
sizeListener.addedRows(-1);
changeListener.changed(source, desc, null);
parent.getParent().layout();
}
} );
newControls.add(number); newControls.add(widget); newControls.add(butt);
}
addNewValueRow(parent, desc, source, textWidget, changeListener, sizeListener, newControls, i);
return newControls;
}
/**
* Override in subclasses as necessary
* @param desc
* @param rule
* @return
*/
protected boolean canAddNewRowFor(final PropertyDescriptor<?> desc, final PropertySource source) {
return true;
}
private void addNewValueRow(final Composite parent, final PropertyDescriptor<?> desc, final PropertySource source, final Text parentWidget, final ValueChangeListener changeListener, final SizeChangeListener sizeListener, final List<Control> newControls, int i) {
if (!canAddNewRowFor(desc, source)) return;
final Label number = new Label(parent, SWT.NONE);
number.setText(Integer.toString(i+1));
newControls.add(number);
final Control widget = addWidget(parent, null, desc, source);
widget.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
newControls.add(widget);
final Button butt = new Button(parent, SWT.PUSH);
butt.setText("+"); // TODO use icon for consistent width
newControls.add(butt);
Listener addListener = new Listener() {
public void handleEvent(Event event) { // add new value handler
// add the new value to the property set
// set the value in the widget to the cleaned up one, disable it
// remove old listener from button, add new (delete) one, update text/icon
// add new row widgets: label, value widget, button
// add listener for new button
Object newValue = addValueIn(widget, desc, source);
if (newValue == null) return;
addNewValueRow(parent, desc, source, parentWidget, changeListener, sizeListener, newControls, -1);
convertToDelete(butt, newValue, parent, newControls, desc, source, parentWidget, number, widget, changeListener, sizeListener);
widget.setEnabled(false);
setValue(widget, newValue);
renumberLabelsIn(newControls);
fillWidget(parentWidget, desc, source);
adjustRendering(source, desc, parentWidget);
sizeListener.addedRows(1);
changeListener.changed(source, desc, newValue);
parent.getParent().layout();
}
};
butt.addListener(SWT.Selection, addListener);
widget.addListener(SWT.DefaultSelection, addListener); // allow for CR on entry widgets themselves, no need to click the '+' button
widget.setFocus();
}
private void convertToDelete(final Button button, final Object toDeleteValue, final Composite parent, final List<Control> newControls, final PropertyDescriptor<?> desc, final PropertySource source, final Text parentWidget, final Label number, final Control widget, final ValueChangeListener changeListener, final SizeChangeListener sizeListener) {
button.setText("-");
Util.removeListeners(button,SWT.Selection);
button.addListener(SWT.Selection, new Listener() {
public void handleEvent(Event event) {
delete(number, widget, button, newControls, toDeleteValue, desc, source);
fillWidget(parentWidget, desc, source);
sizeListener.addedRows(-1);
changeListener.changed(source, desc, null);
parent.getParent().layout();
}
} );
}
private static void renumberLabelsIn(List<Control> controls) {
int i=1;
for (Control control : controls) {
if (control instanceof Label) {
((Label)control).setText(Integer.toString(i++));
}
}
}
protected void fillWidget(Text textWidget, PropertyDescriptor<?> desc, PropertySource source) {
Object[] values = (Object[])valueFor(source, desc);
textWidget.setText(values == null ? "" : StringUtil.asString(values, delimiter + ' '));
adjustRendering(source, desc, textWidget);
}
protected String[] textWidgetValues(Text textWidget) {
String values = textWidget.getText().trim();
if (StringUtil.isEmpty(values)) return StringUtil.getEmptyStrings();
String[] valueSet = values.split(delimiter);
List<String> valueList = new ArrayList<String>(valueSet.length);
for (String value : valueSet) {
String str = value.trim();
if (str.length() > 0) valueList.add(str);
}
return valueList.toArray(new String[valueList.size()]);
}
}