Tuesday, June 29, 2010

Sapphire - Focus on Browsing

If you have missed the introduction to Sapphire, make sure to read it first... Introduction to Sapphire

One of the most enduring UI patterns is a browse button next to the text box for selecting among possible values. Very frequently the scenario is to browse for files or folders, but the pattern is more generic than that and has been used to browse for arbitrary items especially when the set of possible values can be large.

In Sapphire, developers are not creating and wiring up individual UI widgets. This makes it possible to implement the browse button pattern at a higher level of abstraction. If a browse handler is active for a property, a browse button will be automatically created. The framework will even register a keyboard shortcut (Ctrl+L, 'L' is for locate) which can be used to open the browse dialog while focus is on the text field.

Sapphire uses image-based buttons for compactness and to create a more modern look-n-feel. In the following screen capture you can see how the browse buttons appear to the user. Note a tiny browse image in the table cell editor. That's a browse button too.

browse-buttons

File System Paths

Sapphire provides a set of annotations that make it easier to deal with file system paths. The developer uses these annotations to specify the semantics of the property and Sapphire automatically adds validation and browsing support.

Consider the case where a property must hold an absolute path to a file that must exist and must have "jar" or "zip" extension. Such a property could be declared as follows:

@Type( base = IPath.class )
@AbsolutePath
@ValidFileSystemResourceType( FileSystemResourceType.FILE )
@ValidFileExtensions( { "jar", "zip" } )
@MustExist
    
ValueProperty PROP_ABSOLUTE_FILE_PATH = new ValueProperty( TYPE, "AbsoluteFilePath" );
    
Value<IPath> getAbsoluteFilePath();
void setAbsoluteFilePath( String value );
void setAbsoluteFilePath( IPath value );

Based on the above specification, the framework will attach validation that will make sure that the entered path is absolute, that it references a file, that the referenced file exists and that it has the appropriate extension. That happens in the model layer. The UI framework will see these annotations and supply a browse button wired to open the operating system's native file browse dialog pre-filtered to only show jar and zip files.

Similar support is available for absolute folder paths. Just remove @ValidFileExtensions and change @ValidFileSystemResourceType.

Or maybe you are writing an extension to Eclipse IDE and your property needs to hold a workspace path instead of an absolute path... Just replace @AbsolutePath with @EclipseWorkspacePath in the above example. The validation will change to use Eclipse resources API and the native browse dialog will be replaced with the standard Eclipse workspace resources dialog.

Or maybe you need to deal with relative paths, but you have custom requirements for how these relative paths are to be resolved. Sapphire still got you covered. Just replace @AbsolutePath with @BasePathsProvider annotations and implement a class that returns all possible roots...

@Type( base = IPath.class )
@BasePathsProvider( CustomBasePathsProvider.class )
@ValidFileSystemResourceType( FileSystemResourceType.FILE )
@ValidFileExtensions( "dll" )
@MustExist
    
ValueProperty PROP_RELATIVE_FILE_PATH = new ValueProperty( TYPE, "RelativeFilePath" );
    
Value<IPath> getRelativeFilePath();
void setRelativeFilePath( String value );
void setRelativeFilePath( IPath value );
public final class CustomBasePathsProvider extends BasePathsProviderImpl
{
    @Override
    public List<IPath> getBasePaths( IModelElement element )
    {
        final List<IPath> roots = new ArrayList<IPath>();
        
        roots.add( new Path( "c:/Windows" ) );
        roots.add( new Path( "c:/Program Files" ) );

        return roots;
    }
}

You will still get all the validation that you would get with an absolute path, including validation for existence which will try to locate your path using the roots returned by your base paths provider. On the UI side you will get a custom browse dialog box that lets you browse for resources in all the roots simultaneously. This can be very powerful in many contexts where the system that UI is being built for searches for the specified file in a set of defined locations.

relative-path 

String Values

Another common scenario is the case where the value must come from a list possible values not necessarily tied to something specific like file system resources. For instance, consider the case where a property must reference an entity name from the set of entities defined elsewhere.

Sapphire provides a set of three annotations to simplify these scenarios. The annotations are @PossibleValuesProvider, @PossibleValues and @PossibleValuesFromModel. Of the three annotations, the first one is the most generic one. It lets the developer implement a class that computes the set of possible values at runtime...

@PossibleValuesProvider( impl = CityNameValuesProvider.class )
    
ValueProperty PROP_CITY = new ValueProperty( TYPE, "City" );
    
Value<String> getCity();
void setCity( String value );
public class CityNameValuesProvider extends PossibleValuesProviderImpl
{
    @Override
    protected abstract void fillPossibleValues( SortedSet values )
    {
        // Your logic goes here.
    }
}

If you find that in your scenario the set of possible values is static you can use the @PossibleValues annotation instead. This annotation lets you specify the set of possible values right in the annotation instead of implementing a custom values provider.

Or maybe your scenario calls for a property to draw its possible values from another property in the model. The @PossibleValuesFromModel annotation has you covered. It lets you specify a path through the model where possible values should be harvested.

@PossibleValuesFromModel( path = "/Contacts/Name", caseSensitive = false ) 
    
ValueProperty PROP_ASSISTANT = new ValueProperty( TYPE, "Assistant" );
    
Value<String> getAssistant();
void setAssistant( String value );

Regardless of which of the three annotations you use, you will get validation that will check that the specified value is in the set of possible values. Additional attributes are available on all three of these annotations that let you customize the validation. For instance, you can change the problem severity to warning or even disable validation completely. You can even specify whether the comparison should be case sensitive. On the UI front, you will get browse button wired to the standard list item selection dialog.

possible-values 

Java Types

Sapphire even integrates with JDT to support properties that reference classes or interfaces visible to a given Java project. The developer uses the supplied JavaTypeName class as the type for a value property and then tunes the semantics using @JavaTypeConstraints and @MustExist annotations. Sapphire takes care of the rest. You get validation for type existence, kind of type (interface, class, etc.) and even whether type derives from another type. On the UI side, you get a browse button wired to JDT's type selection dialog.

In the following example, the property is specified to take a name of a non-abstract class that must extend AbstractList class while also implementing Cloneable interface.

@Type( base = JavaTypeName.class )
@JavaTypeConstraints( kind = JavaTypeKind.CLASS, type = { "java.util.AbstractList", "java.lang.Cloneable" } )
@MustExist
    
ValueProperty PROP_CUSTOM_LIST_CLASS = new ValueProperty( TYPE, "CustomListClass" );
    
Value<JavaTypeName> getCustomListClass();
void setCustomListClass( String value );
void setCustomListClass( JavaTypeName value );

java-type 

Completely Custom

Sapphire browse handling support is extensible to support cases that do not fit one of the above molds. To do this, you create a custom class that extends BrowseHandler. You can then register your browse handler globally (to activate under a condition that you specify) or locally for a specific property editor in the UI definition. The second case is more common.

Here is an example:

<property-editor>
  <property>Photo</property>
  <browse-handler>
    <class>PhotosCatalogBrowseHandler</class>
  </browse-handler>
</property-handler>

Multi-Way

One variant of the browse button pattern has baffled UI writers for years. In some cases, the semantics of the property require the use of more than one browse dialog. For instance, consider the case where the property can take an absolute path to an archive file or a folder. No established convention exists for how to handle this case and developers have tried a number of different options. Here are a few examples from Eclipse itself.

multi-way-1 

multi-way-2 

multi-way-3

Sapphire adopts the convention of using a drop-down menu from the browse button when multiple browse handlers are active concurrently. Here is what that looks like:

multi-way-sapphire

Currently, there are no model annotations that can fully describe the complex semantics of such scenarios. The developer must register the browse handlers in the UI definition. Validation should be done in a custom validator class attached via @Validator annotation.

Here is the UI definition from the above screen capture. All the system-provided browse handlers that activate when certain annotations are used are also available for direct reference from the UI definitions as can be seen in this example.

<property-editor>
  <property>MultiOptionPath</property>
  <browse-handler>
    <class>AbsoluteFilePathValueBrowseHandler</class>
    <param>
      <name>extensions</name>
      <value>jar,zip</value>
    </param>
  </browse-handler>
  <browse-handler>
    <class>AbsoluteFolderPathValueBrowseHandler</class>
  </browse-handler>
  <browse-handler>
    <class>EclipseWorkspacePathValueBrowseHandler</class>
    <param>
      <name>extensions</name>
      <value>jar,zip</value>
    </param>
    <param>
      <name>leading-slash</name>
      <value>true</value>
    </param>
  </browse-handler>
</property-editor>

No comments: