org.metawidget.swt.SwtMetawidget Maven / Gradle / Ivy
// Metawidget
//
// For historical reasons, this file is licensed under the LGPL
// (http://www.gnu.org/licenses/lgpl-2.1.html).
//
// Most other files in Metawidget are licensed under both the
// LGPL/EPL and a commercial license. See http://metawidget.org
// for details.
package org.metawidget.swt;
import static org.metawidget.inspector.InspectionResultConstants.*;
import java.beans.Beans;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.metawidget.iface.Immutable;
import org.metawidget.iface.MetawidgetException;
import org.metawidget.inspectionresultprocessor.iface.InspectionResultProcessor;
import org.metawidget.inspector.iface.Inspector;
import org.metawidget.layout.iface.Layout;
import org.metawidget.pipeline.w3c.W3CPipeline;
import org.metawidget.swt.layout.SwtLayoutDecorator;
import org.metawidget.util.ArrayUtils;
import org.metawidget.util.ClassUtils;
import org.metawidget.util.CollectionUtils;
import org.metawidget.util.simple.PathUtils;
import org.metawidget.util.simple.PathUtils.TypeAndNames;
import org.metawidget.util.simple.StringUtils;
import org.metawidget.widgetbuilder.composite.CompositeWidgetBuilder;
import org.metawidget.widgetbuilder.iface.WidgetBuilder;
import org.metawidget.widgetprocessor.iface.WidgetProcessor;
import org.w3c.dom.Element;
/**
* Metawidget for SWT environments.
*
* @author Stefan Ackermann, Richard Kennard
*/
public class SwtMetawidget
extends Composite {
//
// Private members
//
private Object mToInspect;
private String mInspectionPath;
private ResourceBundle mBundle;
private boolean mNeedToBuildWidgets;
private Element mLastInspectionResult;
private Map mFacets = CollectionUtils.newHashMap();
/**
* List of existing, manually added, but unused by Metawidget controls.
*
* This is a List, not a Set, for consistency during endBuild.
*/
private List mExistingUnusedControls = CollectionUtils.newArrayList();
private Set mControlsToDispose = CollectionUtils.newHashSet();
/* package private */Composite mCurrentLayoutComposite;
private Pipeline mPipeline;
//
// Constructor
//
public SwtMetawidget( Composite parent, int style ) {
super( parent, style );
mPipeline = newPipeline();
// This covers most cases
addControlListener( new ControlListener() {
public void controlResized( ControlEvent event ) {
buildWidgets();
}
public void controlMoved( ControlEvent event ) {
buildWidgets();
}
} );
// This covers, say, clicking 'Edit' and going from read-only to non-read-only
addPaintListener( new PaintListener() {
public void paintControl( PaintEvent event ) {
if ( event.count == 0 ) {
buildWidgets();
}
// When used as part of an IDE builder tool, render as a dotted square so that we
// can see something!
if ( Beans.isDesignTime() ) {
event.gc.setLineDash( new int[] { 5, 5 } );
event.gc.drawRectangle( 0, 0, event.width - 1, event.height - 1 );
Point textExtent = event.gc.textExtent( "Metawidget" );
event.gc.drawText( "Metawidget", 10, ( event.height - textExtent.y ) / 2 );
}
}
} );
}
//
// Public methods
//
/**
* Sets the Object to inspect.
*
* If setInspectionPath
has not been set, or points to a previous
* setToInspect
, sets it to point to the given Object.
*/
public void setToInspect( Object toInspect ) {
updateToInspectWithoutInvalidate( toInspect );
invalidateInspection();
}
/**
* Updates the Object to inspect, without invalidating the previous inspection results.
*
* This is an internal API exposed for WidgetProcessor rebinding support. Clients should
* not call it directly.
*/
public void updateToInspectWithoutInvalidate( Object toInspect ) {
if ( mToInspect == null ) {
if ( mInspectionPath == null && toInspect != null ) {
mInspectionPath = toInspect.getClass().getName();
}
} else if ( mToInspect.getClass().getName().equals( mInspectionPath ) ) {
if ( toInspect == null ) {
mInspectionPath = null;
} else {
mInspectionPath = toInspect.getClass().getName();
}
}
mToInspect = toInspect;
}
/**
* Gets the Object being inspected.
*
* Exposed for binding implementations.
*
* @return the object. Note this return type uses generics, so as to not require a cast by the
* caller (eg. Person p = getToInspect()
)
*/
@SuppressWarnings( "unchecked" )
public T getToInspect() {
return (T) mToInspect;
}
/**
* Sets the path to be inspected.
*/
// REFACTOR: shouldn't this be setPath?
public void setInspectionPath( String inspectionPath ) {
mInspectionPath = inspectionPath;
invalidateInspection();
}
public String getInspectionPath() {
return mInspectionPath;
}
public void setConfig( String config ) {
mPipeline.setConfig( config );
invalidateInspection();
}
public void setInspector( Inspector inspector ) {
mPipeline.setInspector( inspector );
invalidateInspection();
}
/**
* Useful for WidgetBuilders to perform nested inspections (eg. for Collections).
*/
public String inspect( Object toInspect, String type, String... names ) {
return mPipeline.inspect( toInspect, type, names );
}
public void addInspectionResultProcessor( InspectionResultProcessor inspectionResultProcessor ) {
mPipeline.addInspectionResultProcessor( inspectionResultProcessor );
invalidateInspection();
}
public void removeInspectionResultProcessor( InspectionResultProcessor inspectionResultProcessor ) {
mPipeline.removeInspectionResultProcessor( inspectionResultProcessor );
invalidateInspection();
}
public void setInspectionResultProcessors( InspectionResultProcessor... inspectionResultProcessors ) {
mPipeline.setInspectionResultProcessors( inspectionResultProcessors );
invalidateInspection();
}
public void setWidgetBuilder( WidgetBuilder widgetBuilder ) {
mPipeline.setWidgetBuilder( widgetBuilder );
invalidateWidgets();
}
public void addWidgetProcessor( WidgetProcessor widgetProcessor ) {
mPipeline.addWidgetProcessor( widgetProcessor );
invalidateWidgets();
}
public void removeWidgetProcessor( WidgetProcessor widgetProcessor ) {
mPipeline.removeWidgetProcessor( widgetProcessor );
invalidateWidgets();
}
public void setWidgetProcessors( WidgetProcessor... widgetProcessors ) {
mPipeline.setWidgetProcessors( widgetProcessors );
invalidateWidgets();
}
public T getWidgetProcessor( Class widgetProcessorClass ) {
buildWidgets();
return mPipeline.getWidgetProcessor( widgetProcessorClass );
}
/**
* Set the layout for this Metawidget.
*
* Named setMetawidgetLayout
, rather than the usual setLayout
, because
* SWT already defines a setLayout
. Overloading SWT's setLayout
was
* considered cute, but ultimately confusing and dangerous. For example, what should
* setLayout( null )
do?
*/
public void setMetawidgetLayout( Layout layout ) {
mPipeline.setLayout( layout );
invalidateWidgets();
}
public void setBundle( ResourceBundle bundle ) {
mBundle = bundle;
invalidateWidgets();
}
/**
* Returns a label for the given set of attributes.
*
* The label is determined using the following algorithm:
*
*
* - if attributes.get( "label" ) exists...
*
* - attributes.get( "label" ) is camel-cased and used as a lookup into
* getLocalizedKey( camelCasedLabel ). This means developers can initially build their
* UIs without worrying about localization, then turn it on later
* - if no such lookup exists, return attributes.get( "label" )
*
*
* - if attributes.get( "label" ) does not exist...
*
* - attributes.get( "name" ) is used as a lookup into
* getLocalizedKey( name )
* - if no such lookup exists, return attributes.get( "name" )
*
*
*
*/
public String getLabelString( Map attributes ) {
if ( attributes == null ) {
return "";
}
// Explicit label
String label = attributes.get( LABEL );
if ( label != null ) {
// (may be forced blank)
if ( "".equals( label ) ) {
return null;
}
// (localize if possible)
String localized = getLocalizedKey( StringUtils.camelCase( label ) );
if ( localized != null ) {
return localized.trim();
}
return label.trim();
}
// Default name
String name = attributes.get( NAME );
if ( name != null ) {
// (localize if possible)
String localized = getLocalizedKey( name );
if ( localized != null ) {
return localized.trim();
}
return StringUtils.uncamelCase( name );
}
return "";
}
/**
* @return null if no bundle, ???key??? if bundle is missing a key
*/
public String getLocalizedKey( String key ) {
if ( mBundle == null ) {
return null;
}
try {
return mBundle.getString( key );
} catch ( MissingResourceException e ) {
return StringUtils.RESOURCE_KEY_NOT_FOUND_PREFIX + key + StringUtils.RESOURCE_KEY_NOT_FOUND_SUFFIX;
}
}
public boolean isReadOnly() {
return mPipeline.isReadOnly();
}
public void setReadOnly( boolean readOnly ) {
if ( mPipeline.isReadOnly() == readOnly ) {
return;
}
mPipeline.setReadOnly( readOnly );
invalidateWidgets();
}
public int getMaximumInspectionDepth() {
return mPipeline.getMaximumInspectionDepth();
}
public void setMaximumInspectionDepth( int maximumInspectionDepth ) {
mPipeline.setMaximumInspectionDepth( maximumInspectionDepth );
invalidateWidgets();
}
/**
* Gets the value from the Control with the given name.
*
* The value is returned as it was stored in the Control (eg. String for JTextField) so may need
* some conversion before being reapplied to the object being inspected. This obviously requires
* knowledge of which Control SwtMetawidget created, which is not ideal, so clients may prefer
* to use a binding WidgetProcessor instead.
*
* @return the value. Note this return type uses generics, so as to not require a cast by the
* caller (eg. String s = getValue(names)
)
*/
@SuppressWarnings( "unchecked" )
public T getValue( String... names ) {
ControlAndValueProperty controlAndValueProperty = getControlAndValueProperty( names );
return (T) ClassUtils.getProperty( controlAndValueProperty.getControl(), controlAndValueProperty.getValueProperty() );
}
/**
* Sets the Control with the given name to the specified value.
*
* Clients must ensure the value is of the correct type to suit the Control (eg. String for
* JTextField). This obviously requires knowledge of which Control SwtMetawidget created, which
* is not ideal, so clients may prefer to use a binding WidgetProcessor instead.
*/
public void setValue( Object value, String... names ) {
ControlAndValueProperty controlAndValueProperty = getControlAndValueProperty( names );
ClassUtils.setProperty( controlAndValueProperty.getControl(), controlAndValueProperty.getValueProperty(), value );
}
/**
* Returns the property used to get/set the value of the control.
*
* If the control is not known, returns null
. Does not throw an Exception, as we
* want to fail gracefully if, say, someone tries to bind to a JPanel.
*/
public String getValueProperty( Control control ) {
return getValueProperty( control, mPipeline.getWidgetBuilder() );
}
/**
* Finds the Control with the given name.
*/
@SuppressWarnings( "unchecked" )
public T getControl( String... names ) {
if ( names == null || names.length == 0 ) {
return null;
}
Control topControl = this;
for ( int loop = 0, length = names.length; loop < length; loop++ ) {
String name = names[loop];
// May need building 'just in time' if we are calling getControl
// immediately after a 'setToInspect'. See
// SwtMetawidgetTest.testNestedWithManualInspector
if ( topControl instanceof SwtMetawidget ) {
( (SwtMetawidget) topControl ).buildWidgets();
}
// Try to find a control
topControl = getControl( (Composite) topControl, name );
if ( loop == length - 1 ) {
return (T) topControl;
}
if ( topControl == null ) {
throw MetawidgetException.newException( "No such control '" + name + "' of '" + ArrayUtils.toString( names, "', '" ) + "'" );
}
}
return (T) topControl;
}
public Facet getFacet( String name ) {
buildWidgets();
return mFacets.get( name );
}
/**
* This method is public for use by WidgetBuilders to attach Controls to the current Composite
* as defined by the Layout. This allows the Layout to introduce new Composites, such as for
* TabFolders.
*/
public Composite getCurrentLayoutComposite() {
if ( mCurrentLayoutComposite == null ) {
return this;
}
return mCurrentLayoutComposite;
}
//
// The following methods all kick off buildWidgets() if necessary
//
@Override
public org.eclipse.swt.widgets.Layout getLayout() {
buildWidgets();
return super.getLayout();
}
@Override
public Control[] getChildren() {
buildWidgets();
return super.getChildren();
}
//
// Protected methods
//
/**
* Instantiate the Pipeline used by this Metawidget.
*
* Subclasses wishing to use their own Pipeline should override this method to instantiate their
* version.
*/
protected Pipeline newPipeline() {
return new Pipeline();
}
protected String getDefaultConfiguration() {
return ClassUtils.getPackagesAsFolderNames( SwtMetawidget.class ) + "/metawidget-swt-default.xml";
}
/**
* Invalidates the current inspection result (if any) and invalidates the widgets.
*
* As an optimisation we only invalidate the widgets, not the entire inspection result, for some
* operations (such as adding/removing stubs, changing read-only etc.)
*/
protected void invalidateInspection() {
mLastInspectionResult = null;
invalidateWidgets();
}
/**
* Invalidates the widgets.
*/
protected void invalidateWidgets() {
if ( mNeedToBuildWidgets ) {
return;
}
mNeedToBuildWidgets = true;
}
protected void buildWidgets() {
// No need to build?
if ( !mNeedToBuildWidgets || Beans.isDesignTime() ) {
return;
}
mPipeline.configureOnce();
mNeedToBuildWidgets = false;
// Metawidget needs a way to distinguish between manually added controls and generated
// controls: the generated ones must be cleaned up on subsequent buildWidgets(), whereas
// the manual ones must be left alone. SWT doesn't appear to have a mechanism for listening
// for child add/remove events (as we use in Android, GWT, Swing etc), so instead we
// implement this as the delta of 'what was here originally' versus 'what was generated'
for ( Control control : mControlsToDispose ) {
control.dispose();
}
mControlsToDispose.clear();
mExistingUnusedControls = CollectionUtils.newArrayList( getChildren() );
Set existingControls = CollectionUtils.newHashSet( mExistingUnusedControls );
// Detect facets
for ( Control control : getChildren() ) {
if ( control instanceof Facet ) {
mFacets.put( (String) control.getData( NAME ), (Facet) control );
continue;
}
}
// Build widgets
try {
if ( mLastInspectionResult == null ) {
mLastInspectionResult = inspect();
}
mPipeline.buildWidgets( mLastInspectionResult );
// Work out the delta of 'what was here originally' versus 'what was generated'
//
// Note: we cannot simply do this in layoutWidget, because some controls may get created
// just-in-time, such as Labels
for ( Control control : getChildren() ) {
if ( !existingControls.remove( control ) ) {
mControlsToDispose.add( control );
}
}
// Layout up the heirarchy so that all parents are laid out correctly (we're not sure of
// the 'correctness' of this - it's just what worked after trial and error)
Composite topParent = getParent();
while ( topParent != null ) {
topParent.layout();
topParent = topParent.getParent();
}
} catch ( Exception e ) {
throw MetawidgetException.newException( e );
}
}
/**
* @param elementName
* XML node name of the business field. Typically 'entity', 'property' or 'action'.
* Never null
*/
protected void layoutWidget( Control control, String elementName, Map attributes ) {
// Set the name of the component.
control.setData( NAME, attributes.get( NAME ) );
// Re-order the component
control.moveBelow( null );
mExistingUnusedControls.remove( control );
control.setLayoutData( null );
// Look up any additional attributes
Map additionalAttributes = mPipeline.getAdditionalAttributes( control );
if ( additionalAttributes != null ) {
attributes.putAll( additionalAttributes );
}
// BasePipeline will call .layoutWidget
}
protected void endBuild() {
for ( Control existingControl : CollectionUtils.newArrayList( mExistingUnusedControls ) ) {
// Unused facets don't count
if ( existingControl instanceof Facet ) {
existingControl.moveBelow( null );
continue;
}
// Manually created components default to no section
Map attributes = CollectionUtils.newHashMap();
attributes.put( SECTION, "" );
mPipeline.layoutWidget( existingControl, PROPERTY, attributes );
}
}
protected void initNestedMetawidget( SwtMetawidget nestedMetawidget, Map attributes ) {
// Don't copy setConfig(). Instead, copy runtime values
mPipeline.initNestedPipeline( nestedMetawidget.mPipeline, attributes );
nestedMetawidget.setInspectionPath( mInspectionPath + StringUtils.SEPARATOR_FORWARD_SLASH_CHAR + attributes.get( NAME ) );
nestedMetawidget.setBundle( mBundle );
nestedMetawidget.setToInspect( mToInspect );
}
//
// Private methods
//
private Element inspect() {
if ( mInspectionPath == null ) {
return null;
}
TypeAndNames typeAndNames = PathUtils.parsePath( mInspectionPath );
return mPipeline.inspectAsDom( mToInspect, typeAndNames.getType(), typeAndNames.getNamesAsArray() );
}
private ControlAndValueProperty getControlAndValueProperty( String... names ) {
Control control = getControl( names );
if ( control == null ) {
throw MetawidgetException.newException( "No control named '" + ArrayUtils.toString( names, "', '" ) + "'" );
}
String valueProperty = getValueProperty( control );
if ( valueProperty == null ) {
throw MetawidgetException.newException( "Don't know how to getValue from a " + control.getClass().getName() );
}
return new ControlAndValueProperty( control, valueProperty );
}
private String getValueProperty( Control control, WidgetBuilder widgetBuilder ) {
// Recurse into CompositeWidgetBuilders
try {
if ( widgetBuilder instanceof CompositeWidgetBuilder, ?> ) {
for ( WidgetBuilder widgetBuilderChild : ( (CompositeWidgetBuilder) widgetBuilder ).getWidgetBuilders() ) {
String valueProperty = getValueProperty( control, widgetBuilderChild );
if ( valueProperty != null ) {
return valueProperty;
}
}
return null;
}
} catch ( NoClassDefFoundError e ) {
// May not be shipping with CompositeWidgetBuilder
}
// Interrogate ValuePropertyProviders
if ( widgetBuilder instanceof SwtValuePropertyProvider ) {
return ( (SwtValuePropertyProvider) widgetBuilder ).getValueProperty( control );
}
return null;
}
private Control getControl( Composite container, String name ) {
for ( Control childComponent : container.getChildren() ) {
// Drill into unnamed containers (ie. for TabFolders)
if ( childComponent.getData( NAME ) == null && childComponent instanceof Composite ) {
childComponent = getControl( (Composite) childComponent, name );
if ( childComponent != null ) {
return childComponent;
}
continue;
}
// Match by name
if ( name.equals( childComponent.getData( NAME ) ) ) {
return childComponent;
}
}
// Not found
return null;
}
//
// Inner class
//
protected class Pipeline
extends W3CPipeline {
//
// Protected methods
//
@Override
protected SwtMetawidget getPipelineOwner() {
return SwtMetawidget.this;
}
@Override
protected String getDefaultConfiguration() {
return SwtMetawidget.this.getDefaultConfiguration();
}
@Override
protected void configure() {
// Special support for visual IDE builders
if ( Beans.isDesignTime() ) {
return;
}
super.configure();
}
@Override
protected void configureDefaults() {
super.configureDefaults();
// SwtMetawidget uses setMetawidgetLayout, not setLayout
if ( getLayout() == null ) {
getConfigReader().configure( getDefaultConfiguration(), getPipelineOwner(), "metawidgetLayout" );
}
}
@Override
protected Control buildWidget( String elementName, Map attributes ) {
if ( !ENTITY.equals( elementName ) ) {
Layout layout = getLayout();
if ( layout instanceof SwtLayoutDecorator ) {
mCurrentLayoutComposite = ( (SwtLayoutDecorator) layout ).startBuildWidget( elementName, attributes, SwtMetawidget.this, SwtMetawidget.this );
}
}
return super.buildWidget( elementName, attributes );
}
@Override
protected void layoutWidget( Control control, String elementName, Map attributes ) {
SwtMetawidget.this.layoutWidget( control, elementName, attributes );
super.layoutWidget( control, elementName, attributes );
}
@Override
protected Map getAdditionalAttributes( Control control ) {
if ( control instanceof Stub ) {
return ( (Stub) control ).getAttributes();
}
return null;
}
@Override
public SwtMetawidget buildNestedMetawidget( Map attributes )
throws Exception {
SwtMetawidget nestedMetawidget = SwtMetawidget.this.getClass().getConstructor( Composite.class, int.class ).newInstance( getPipelineOwner().getCurrentLayoutComposite(), SWT.None );
SwtMetawidget.this.initNestedMetawidget( nestedMetawidget, attributes );
return nestedMetawidget;
}
@Override
protected void endBuild() {
SwtMetawidget.this.endBuild();
super.endBuild();
}
}
/**
* Simple immutable structure to store a component and its value property.
*
* @author Richard Kennard
*/
private static class ControlAndValueProperty
implements Immutable {
//
// Private members
//
private Control mControl;
private String mValueProperty;
//
// Constructor
//
public ControlAndValueProperty( Control control, String valueProperty ) {
mControl = control;
mValueProperty = valueProperty;
}
//
// Public methods
//
public Control getControl() {
return mControl;
}
public String getValueProperty() {
return mValueProperty;
}
}
}