Stan Lee in Spider Man taught us that, "With great power there must also come -- great responsibility!" I think about that quote when working on Eclipse plug-ins, only it changes in my head to read, "With great power there must also come -- great complexity!" Eclipse provides the building blocks to do wonderfully powerful things, but to wade into plug-in development is to wade into all sorts of complexity.
My most recent responsibility/complexity was to make a service properties chooser/editor in the BUG Application code generation wizard. This functionality (still in development at the time of this writing), will allow BUG Application developers tighter control of the filters used by the Application's Service Tracker.
Service property values are stored as strings, but they can represent booleans, numbers, or Strings. I wanted to create a table where each row is a service property key/value pair. The developer can then choose the properties they want to include in the filter and modify the values for that property. Moreover, I wanted the property value to be editable like a text field for things like numbers, but use a combo box for things like boolean values. Here's what the table looks like:
It looks straight forward enough, but the implementation turned out to be rather challenging. The custom behavior of the cells pushed me toward using JFace CellEditorS and EditingSupport. I found some tutorials and code-snippets on-line, but nothing suited my specific needs, which were to have checkbox support and selection events on the rows, plus different CellEditorS for each of the second column's cells, depending on the type of value. The most helpful tutorial I found is here: http://www.vogella.de/articles/EclipseJFaceTable/article.html . It didn't solve all my problems, but it certainly gave me a start.
After some time-consuming web-slinging, pining over the above tutorial, and some old-fashion trial and error, I was able to finally make the thing work. Here, I share my solution in the hope that it will help where other resources fall short:
First, let's start out with the class. This particular JFace component was added to a WizardPage:
public class CodeGenerationPage extends WizardPage {
Next, define our main TableViewer as an instance variable:
// Main JFace component private CheckboxTableViewer servicePropertiesViewer;
All of the wizard page drawing is kicked off from the createControl method, which you must override. Inside there, we create the JFace components -- a CheckboxTableViewer which is the surrounding table, and TableColumnViewerS for the columns -- and put it all together:
// table with list of properties to choose from // compServices is a Group that we're putting all of this stuff in final Table propertiesTable = new Table( compServices, SWT.CHECK | SWT.BORDER | SWT.V_SCROLL | SWT.FULL_SELECTION); propertiesTable.setHeaderVisible(true); propertiesTable.setLinesVisible(true); // layout of columns in table TableLayout propTableLayout = new TableLayout(); propTableLayout.addColumnData(new ColumnWeightData(90)); propTableLayout.addColumnData(new ColumnWeightData(120)); propertiesTable.setLayout(propTableLayout); // layout of table on the page GridData pViewerData = new GridData(GridData.FILL_BOTH); pViewerData.horizontalSpan = layout.numColumns; pViewerData.heightHint = SERVICE_PROPERTIES_HEIGHT_HINT; propertiesTable.setLayoutData(pViewerData); // viewer for services list servicePropertiesViewer = new CheckboxTableViewer(propertiesTable); servicePropertiesViewer.setContentProvider(new ServicePropsContentProvider()); // Add a listener to do something when a checkbox on a row is selected servicePropertiesViewer.addCheckStateListener(new ICheckStateListener() { public void checkStateChanged(CheckStateChangedEvent event) { // You can do something when a row is selected here } }); // Column 0 - checkbox and property name // col0 is taken care of by checkboxtableviewer TableViewerColumn col0viewer = new TableViewerColumn(servicePropertiesViewer, SWT.FULL_SELECTION, 0); // TableViewerColumn needs a label provider col0viewer.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { return ((ServicePropertyHelper) element).getKey(); } }); col0viewer.getColumn().setText(KEY_LABEL); // Column 1 - property value w/ celleditors // col1 has custom cell editors defined in EditingSupport below TableViewerColumn col1viewer = new TableViewerColumn(servicePropertiesViewer, SWT.FULL_SELECTION, 1); col1viewer.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { return ((ServicePropertyHelper) element).getSelectedValue(); } }); col1viewer.getColumn().setText(VALUE_LABEL); // col1viewer has editing support // this is where the magic happens that sets a different celleditor depending col1viewer.setEditingSupport( new PropertyValueEditingSupport(col1viewer.getViewer()));
The comments in the code should help explain things, but there are two items worthy of extra attention. First, by using a CheckboxTableViewer, we can get a selection event from the table and also access selected elements via servicePropertiesViewer.getCheckedElements(). For the second column's CellEditorS, we need to decide, per-cell, which editor to use (whether to use a TextCellEditor or a ComboBoxCellEditor) based on what the potential values are. We do this by adding EditingSupport to the column, i.e. col1viewer.setEditingSupport(). Here is most of the EditingSupport implementation (with all the helper functions stripped out for brevity):
public class PropertyValueEditingSupport extends EditingSupport { private final String[] truefalse = new String[] {"true", "false"}; private Composite parent; private TextCellEditor text_editor; private ComboBoxCellEditor combobox_editor; public PropertyValueEditingSupport(ColumnViewer viewer) { super(viewer); parent =((TableViewer) viewer).getTable(); text_editor = new TextCellEditor(parent); combobox_editor = new ComboBoxCellEditor(parent, new String[0]); } @Override protected boolean canEdit(Object element) { return true; } @Override protected CellEditor getCellEditor(Object element) { ServicePropertyHelper propertyHelper = ((ServicePropertyHelper) element); if (usesTextEditor(propertyHelper.getValues())) { // Ints and blanks use text editor return text_editor; } else { // everything else uses a combobox if (hasBools(propertyHelper.getValues())) // boolean combos prefill w/ true and false combobox_editor.setItems(truefalse); else // other types, just do set the combobox to values combobox_editor.setItems(propertyHelper.getValuesAsArray()); return combobox_editor; } } @Override protected Object getValue(Object element) { ServicePropertyHelper propertyHelper = ((ServicePropertyHelper) element); if (usesTextEditor(propertyHelper.getValues())) { return propertyHelper.getSelectedValue(); } else { return Integer.valueOf(propertyHelper.getSelectedIndex()); } } @Override protected void setValue(Object element, Object value) { // Get the current service that's been selected ServicePropertyHelper propertyHelper = ((ServicePropertyHelper) element); if (usesTextEditor(propertyHelper.getValues())) { // if it's a text field, just set the value propertyHelper.setSelectedValue("" + value); } else if (hasBools(propertyHelper.getValues())) { // if it's a boolean, value is an index in truefalse array propertyHelper.setSelectedValue(truefalse[Integer.valueOf("" + value)]); } else { // if it's something else, value is an index in the service property values set String val = propertyHelper.getValueAt(Integer.valueOf("" + value)); if (val != null) propertyHelper.setSelectedValue(val); } getViewer().update(element, null); } }
In the constructor, we create our two CellEditorS. We then override getCellEditor(Object element) to return the proper cell editor for the passed element. Element is an element in the array returned from servicePropertiesViewer's ContentProvider.getElements() method (I must point out that the relationship between a TableViewer, it's ContentProvider, it's TableViewerColumnS, and a TableViewerColumn's EditingSupport is pretty confusing. The tutorial mentioned above should help clear all that up if you're lost. Also, if you're still in the dark about JFace viewers, label providers, content providers, and inputs, Eclipse: Building Commercial-Quality Plug-ins is a must-read). We must also override a couple of other EditingSupport methods: canEdit(), getValue(Object element), and setValue(Object element, Object value). The important thing to note is that the values (given and returned) for a TextCellEditor are Strings, and for a ComboBoxCellEditor, Integers. My model object (when I set up my CheckBoxTableViewer, a list of model objects is set with the setInput() method), is called ServicePropertyHelper. It keeps track of the possible values and the set/selected values for a property. It also has some helper methods for setting these, which the PropertyValueEditingSupport methods call.
So, this is obviously rather complex stuff, but it's powerful as well. Using EditingSupport and CellEditorS gives very fine-grained control over JFace TableViewerS for doing real custom Interfaces. Lastly, the full classes can be found in our svn tree at svn://svn.buglabs.net/dragonfly/trunk/com.buglabs.dragonfly.ui/src/com/buglabs/dragonfly/ui/wizards/bugProject and I'll be happy to answer any questions I can if you find yourself wrangling with similar problems.