WidgetProcessors allow arbitrary processing of a widget following its building by a WidgetBuilder and before its inclusion in the Layout. This section covers WidgetProcessors in general. For in-depth documentation of individual WidgetProcessors see Chapter 7, WidgetProcessors.
All WidgetProcessors must implement the WidgetProcessor interface. This is a simple interface that defines only one method:
W processWidget( W widget, String elementName, Map<String, String> attributes, M metawidget );
Where W is a widget type (such as JComponent or UIComponent) and M is a Metawidget type (such as SwingMetawidget or UIMetawidget).
processWidget is called for each widget built by the WidgetBuilders. WidgetProcessors can modify the given widget according to the given elementName (which is typically just 'property' or 'action' from the inspection-result) and the various attributes (as parsed from the inspection-result). They can use the given metawidget to help them if needed (e.g. to access a UI context with which to create validators).
The processWidget method must return the processed widget. This is typically the same as the given widget. The parent Metawidget then passes this to the next WidgetProcessor in the list as shown in Figure 2.6.
In most cases the WidgetProcessor will simply be modifying the given widget (adding validators, changing styles and so on). However it can decide to swap the widget out by returning a different widget entirely. This new widget will then be passed down the list as shown in Figure 2.7. For an example use of this capability, see HiddenFieldProcessor.
Alternatively, the WidgetProcessor can decide to exclude the widget entirely by returning null. Subsequent WidgetProcessors will not be called, as shown in Figure 2.8, and no widget will be added to the Layout.
Some Metawidgets will instantiate default WidgetProcessors. This default behaviour can be overridden either in code:
metawidget.addWidgetProcessor( new MyWidgetProcessor() );
Or via metawidget.xml (see Section 2.7, “metawidget.xml and ConfigReader”):
<swingMetawidget xmlns="java:org.metawidget.swing"> <widgetProcessors> <array> <myWidgetProcessor xmlns="java:com.myapp"/> </array> </widgetProcessors> </swingMetawidget>
This allows easy plugging in of alternate WidgetProcessors. Note that overriding the default means the default is no longer instantiated. In the example above, this would mean MyWidgetProcessor is used but the default WidgetProcessors are not.
The list of WidgetProcessors is maintained by the parent Metawidget, and is changeable (this is different to say, CompositeInspector or CompositeWidgetBuilder whose lists are immutable). This capability is designed to allow easy attaching of event handlers, and scenarios such as inner classes that have connections to their parent class:
final Object someObject = ...; metawidget.addWidgetProcessor( new WidgetProcessor<JComponent, SwingMetawidget>() { JComponent processWidget( JComponent widget, String elementName, Map<String, String> attributes, SwingMetawidget metawidget ) { ...decide whether to attach event handler... widget.add( new AbstractAction() { public void actionPerformed( ActionEvent e ) { someObject.doSomething(); } } } }
The WidgetProcessor interface only has a single method. This allows it to take advantage of future Java language features such as:
final Object someObject = ...;
metawidget.addWidgetProcessor(
#(JComponent w, String name, Map<String, String> attr, SwingMetawidget m)
{ w.add( #(ActionEvent e) { someObject.doSomething() } ) } );
However for those needing more control over the WidgetProcessor lifecycle there is an extended interface AdvancedWidgetProcessor. This interface defines two additional methods:
void onStartBuild( M metawidget ); void onEndBuild( M metawidget );
The first method, onStartBuild, is called at the start of the widget building process, before the WidgetBuilder is called. WidgetProcessors may wish to act on this event to initialize themselves ready for processing. However it is acceptable to do nothing.
The last method, onEndBuild, is called at the end of the widget building process, after all widgets have been built and added to the Layout. WidgetProcessors may wish to act on this event to clean themselves up following processing. However it is acceptable to do nothing.
Some Metawidgets have default WidgetProcessors. Overriding the default means the default is no longer instantiated. This may not be what you want, so you may want to instantiate the default along with your new WidgetProcessor. You can see the default by looking in the Metawidget JAR for the file metawidget-xxx-default.xml (where xxx is your target platform, such as swing or struts).
For reference, the defaults are:
Platform | Default |
---|---|
Android | <array> <simpleBindingProcessor /> <reflectionBindingProcessor /> <disabledAttributeProcessor /> </array> |
AngularJS | [ new metawidget.widgetprocessor.IdProcessor(), new metawidget.widgetprocessor.PlaceholderAttributeProcessor(), new metawidget.widgetprocessor.DisabledAttributeProcessor(), new metawidget.angular.widgetprocessor.AngularWidgetProcessor(...) ] |
GWT | StyleNameProcessor |
JavaScript | [ new metawidget.widgetprocessor.IdProcessor(), new metawidget.widgetprocessor.RequiredAttributeProcessor(), new metawidget.widgetprocessor.PlaceholderAttributeProcessor(), new metawidget.widgetprocessor.DisabledAttributeProcessor(), new metawidget.widgetprocessor.SimpleBindingProcessor() ] |
JQuery UI | [ new metawidget.widgetprocessor.IdProcessor(), new metawidget.widgetprocessor.RequiredAttributeProcessor(), new metawidget.widgetprocessor.PlaceholderAttributeProcessor(), new metawidget.widgetprocessor.DisabledAttributeProcessor(), new metawidget.jqueryui.widgetprocessor.JQueryUIBindingProcessor(), new metawidget.widgetprocessor.SimpleBindingProcessor() ] |
JSF | <array> <requiredAttributeProcessor /> <immediateAttributeProcessor /> <standardBindingProcessor /> <readableIdProcessor /> <labelProcessor /> <standardValidatorProcessor /> <standardConverterProcessor /> <cssStyleProcessor /> </array> |
JSP | (none) |
Spring | <array> <pathProcessor /> <cssStyleProcessor /> </array> |
Struts | <array> <nameProcessor /> <cssStyleProcessor /> </array> |
Swing | <array> <reflectionBindingProcessor /> </array> |
SWT | <array> <reflectionBindingProcessor /> </array> |
Vaadin | <array> <simpleBindingProcessor /> <reflectionBindingProcessor /> <captionProcessor /> <requiredProcessor /> <minimumMaximumValidatorProcessor /> </array> |
All WidgetProcessors are required to be immutable. This means you only need a single instance of a WidgetProcessor for your entire application. If you are using metawidget.xml then ConfigReader takes care of this for you, but if you are instantiating WidgetProcessors in Java code you should reuse instances.
Although individual WidgetProcessors are immutable, the List they are contained in can be changed. Methods such as addWidgetProcessor allows clients to dynamically add WidgetProcessors at runtime. This is useful for adding event handlers (see Section 2.5.1, “Interface”).
Note that immutable only means WidgetProcessors cannot be changed once instantiated - it does not mean they cannot be configured. Many WidgetProcessors have corresponding xxxConfig classes that allow them to be configured prior to instantation in a type-safe way. For example, a BeansBindingProcessor can be configured in code:
metawidget.addWidgetProcessor( new BeansBindingProcessor( new BeansBindingProcessorConfig() .setUpdateStrategy( UpdateStrategy.READ_WRITE )));
Or in metawidget.xml (see Section 2.7, “metawidget.xml and ConfigReader”):
<beansBindingProcessor xmlns="java:org.metawidget.swing.widgetprocessor.binding.beansbinding" config="BeansBindingProcessorConfig"> <updateStrategy> <enum>READ_WRITE</enum> </updateStrategy> </beansBindingProcessor>
Here is an example of a custom WidgetProcessor to add tooltips to JComponents. It extends the code from the tutorial (see Section 1.1, “Part 1 (Java version) - The First Metawidget Application”).
package com.myapp; import static org.metawidget.inspector.InspectionResultConstants.*; import java.util.*; import javax.swing.*; import org.metawidget.swing.*; import org.metawidget.widgetprocessor.iface.*; public class Main { public static void main( String[] args ) { Person person = new Person(); SwingMetawidget metawidget = new SwingMetawidget(); metawidget.addWidgetProcessor( new TooltipProcessor() ); metawidget.setToInspect( person ); JFrame frame = new JFrame( "Metawidget Tutorial" ); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.getContentPane().add( metawidget ); frame.setSize( 400, 250 ); frame.setVisible( true ); } static class TooltipProcessor implements WidgetProcessor<JComponent, SwingMetawidget> { public JComponent processWidget( JComponent widget, String elementName, Map<String, String> attributes, SwingMetawidget metawidget ) { widget.setToolTipText( attributes.get( NAME ) ); return widget; } } }
Like Inspectors, InspectionResultProcessors, WidgetBuilders and Layouts, WidgetProcessors are required to be immutable. However they will generally need to store some internal state, such as tracking the prefix for the tooltip. This can be achieved in two ways:
For state that will remain constant throughout the life of the WidgetProcessor, such as the prefix for the tooltip, use xxxWidgetProcessorConfig classes. For example:
public class TooltipProcessorConfig { private String mPrefix; public TooltipProcessorConfig setPrefix( String prefix ) { mPrefix = prefix; return this; } public String getPrefix() { return mPrefix; } // ...must override equals and hashCode too... }
These xxxWidgetProcessorConfig classes are then passed to the WidgetProcessor at construction time, and stored internally:
public class TooltipProcessor { private String mPrefix; public TooltipProcessor( TooltipProcessorConfig config ) { mPrefix = config.getPrefix(); } }
This mechanism can then be controlled either programmatically:
metawidget.addWidgetProcessor(new TooltipProcessor(new TooltipProcessorConfig().setPrefix("Tip:")));
Or in metawidget.xml (see Section 2.7, “metawidget.xml and ConfigReader”):
<tooltipProcessor xmlns="java:com.foo" config="TooltipProcessorConfig"> <prefix> <string>Tip:</string> </prefix> </tooltipProcessor>
Config classes must override equals and hashCode | |
---|---|
If you want your configurable WidgetProcessor to be cacheable and reusable by ConfigReader and metawidget.xml, the xxxWidgetProcessorConfig class must override equals and hashCode. |
Generate an XML Schema | |
---|---|
If you intend your WidgetProcessor to be configurable via metawidget.xml, consider defining an XML Schema for it. This is optional, but allows users to validate their use of your WidgetProcessor in their metawidget.xml at development time. There is an Ant task, org.metawidget.config.XmlSchemaGeneratorTask, provided in the source distribution that can help with this by auto-generating the schema. All the existing Metawidget schemas are generated using this Ant task. |
For state that will change across multiple widgets, such as BeansBindingProcessor which tracks all bindings so that it can implement a rebind method, store it in the Metawidget that is passed to onStartBuild, processWidget and onEndBuild. You may want to further wrap the state in a small helper class. For example:
public JComponent processWidget( JComponent widget, String elementName, Map<String, String> attributes, SwingMetawidget metawidget ) { getState( container ).bindings.add( attributes.get( NAME )); } private State getState( SwingMetawidget metawidget ) { State state = (State) metawidget.getClientProperty( getClass() ); if ( state == null ) { state = new State(); metawidget.putClientProperty( getClass(), state ); } return state; } static class State { List<String> bindings; ...other state variables... }
You could also consider storing the state in a ThreadLocal. This is straightforward because WidgetProcessors are not re-entrant.