Sapphire Developer Guide > Releases > 0.5

Enhancements in 0.5

  1. Diagrams
    1. Straight to GEF
    2. Flexible Layout Persistence
    3. Context Help
    4. Header
    5. New Actions
    6. Multi-Select Actions
    7. Floating Tool Bar
  2. Forms
    1. Drag-n-Drop
    2. Pop-Up List Field Presentation for Possible Value Properties
    3. Flexible Actuator Presentation
    4. Expression for Property Editor Label
  3. Services
    1. Part Services
    2. AdapterService
    3. DiagramLayoutPersistenceService
    4. DragAndDropService
    5. EqualityService
    6. ListSelectionService
    7. ValidationService at Element Level
  4. Miscellaneous
    1. IModelElement.disposed()
    2. Image Function
    3. Propagated Key Bindings
    4. Action Tool Tips

Straight to GEF

The Graphiti-based diagram presentation (introduced in 0.4 release) has been replaced with a presentation written directly on top of GEF. This has significantly reduced Sapphire's dependencies and enabled diagram support to more tightly integrate with the rest of Sapphire.

Flexible Diagram Layout Persistence

Customize how diagram layout is persisted by implementing DiagramLayoutPersistenceService or take advantage of one of the provided implementations:

  1. side-by-side - Layout is persisted in a separate file alongside the file being edited.
  2. project - Layout is persisted in a semi-hidden file in project's .settings folder.
  3. workspace (default) - Layout is persisted in workspace's .metadata folder.

Example

<diagram-page>
  <layout-persistence>project</layout-persistence>                
</diagram-page>

Example

The architecture sample provides a comprehensive example of a custom DiagramLayoutPersistenceService implementation that persists layout in the same file as data.

<diagram-page>
  <layout-persistence>custom</layout-persistence>
  <service>
    <implementation>ArchitectureDiagramLayoutPersistenceService</implementation>
  </service>
</diagram-page>

Diagram Context Help

Expose context sensitive help in a diagram by annotating the model being edited with @Documentation annotations. When "F1" is pressed in a diagram, context help will come from the model element associated with the selected node or connection. If nothing is selected, the context help will come from the model element associated with the diagram.

Diagram Header

Diagram editor page now displays a header just like the form editor page. The header shows page title and provides another place for page-level actions. To contribute actions to the header, use Sapphire.Diagram.Header context at diagram page level.

Example

<diagram-page>
  <action>
    <id>Sapphire.Samples.OpenSapphireWebSite</id>
    <label>Sapphire Web Site</label>
    <tooltip>Open Sapphire Web Site (Ctrl+Alt+Shift+S)</tooltip>
    <key-binding>CONTROL+ALT+SHIFT+s</key-binding>
    <key-binding-behavior>propagated</key-binding-behavior>
    <context>Sapphire.Diagram.Editor</context>
    <context>Sapphire.Diagram.Header</context>
    <location>after:Sapphire.Diagram.Print</location>
    <group>Sapphire.Samples.OpenSapphireWebSite</group>
    <image>Web.png</image>
    <hint>
      <name>style</name>
      <value>image+text</value>
    </hint>
  </action>
  <action-handler>
    <action>Sapphire.Samples.OpenSapphireWebSite</action>
    <id>Sapphire.Samples.OpenSapphireWebSite</id>
    <label>Sapphire Web Site</label>
    <impl>OpenSapphireWebSiteActionHandler</impl>
  </action-handler>
</diagram-page>

New Diagram Actions

Easily delete all bend points for one or more connections.

Select all diagram parts or just the nodes using a pair of new actions. Alternatively, use Ctrl + A key binding to select everything.

Use zoom actions to magnify or shrink the diagram for a better view. The zoom level is persisted between editing sessions.

Print the diagram or save it as an image to share with others.

Multi-Select Actions in Diagrams

Develop actions that work in the context where multiple nodes and/or connections are selected. Such actions should specify Sapphire.Diagram.MultipleParts context and be contributed at diagram page level.

Example

public class ExampleActionHandler extends SapphireActionHandler
{
    @Override
    protected Object run( SapphireRenderingContext context ) 
    {
        final SapphireDiagramEditorPagePart page = (SapphireDiagramEditorPagePart) getPart();
        
        for( ISapphirePart selectedPart : page.getSelections() )
        {
            if( selectedPart instanceof DiagramNodePart )
            {
                ...
            }
            else if( selectedPart instanceof DiagramConnectionPart )
            {
                ...
            }
        }
        
        ...
        
        return null;
    }
}
<diagram-page>
    <action>
        <id>Example</id>
        <label>example</label>
        <context>Sapphire.Diagram.MultipleParts</context>
    </action>
    <action-handler>
        <action>Example</action>
        <impl>ExampleActionHandler</impl>
    </action-handler>
</diagram-page>

Floating Tool Bar on Diagram Nodes

A floating toolbar is now displayed around a diagram node when the mouse hovers over it. It contains all the actions that are applicable to the node.

Drag-n-Drop in Forms

Use drag-n-drop to arrange the elements in the content outline and in table property editors.

Pop-Up List Field Presentation for Possible Value Properties

Two pop-up list field presentations (editable and strict) have been added for properties with a possible values constraint. The default presentation remains a text field with a browse button and content assist. The new presentations are available for both stand-alone property editors and when a property is edited within a table.

Example

In this example, two properties are used to illustrate the different options. The Color property specifies an error for the severity of deviation from the possible values constraint, while the Shape property specifies a warning.

The default presentation is a text field with a browse button and content assist.

An editable pop-up list field presentation can be used by specifying Sapphire.PropertyEditor.PopUpListField.Editable as the presentation style. This presentation is most appropriate when the severity of deviation from the possible values constraint is something other than an error.

<property-editor>
    <property>Color</property>
    <style>Sapphire.PropertyEditor.PopUpListField.Editable</style>
</property-editor>
<property-editor>
    <property>Shape</property>
    <style>Sapphire.PropertyEditor.PopUpListField.Editable</style>
</property-editor>

A strict pop-up list field presentation can be used by specifying Sapphire.PropertyEditor.PopUpListField.Strict as the presentation style. This presentation is most appropriate when the severity of deviation from the possible values constraint is an error.

<property-editor>
    <property>Color</property>
    <style>Sapphire.PropertyEditor.PopUpListField.Strict</style>
</property-editor>
<property-editor>
    <property>Shape</property>
    <style>Sapphire.PropertyEditor.PopUpListField.Strict</style>
</property-editor>

Alternatively, let the framework choose between editable and strict pop-up list field styles depending on the property's possible values constraint by specifying Sapphire.PropertyEditor.PopUpListField as the presentation style.

<property-editor>
    <property>Color</property>
    <style>Sapphire.PropertyEditor.PopUpListField</style>
</property-editor>
<property-editor>
    <property>Shape</property>
    <style>Sapphire.PropertyEditor.PopUpListField</style>
</property-editor>

The new presentations are accessible in a similar manner when properties are edited within a table.

<property-editor>
    <property>ColoredShapes</property>
    <show-label>false</show-label>
    <span>true</span>
    <child-property>
        <property>Color</property>
        <style>Sapphire.PropertyEditor.PopUpListField</style>
    </child-property>
    <child-property>
        <property>Shape</property>
        <style>Sapphire.PropertyEditor.PopUpListField</style>
    </child-property>
</property-editor>

Flexible Actuator Presentation

An actuator provides means to invoke an action. The action could be drawn from the context where actuator is placed or provided as part of actuator's definition. Actuators were formerly limited to links and were referred to as action links. New for this release is the ability to choose either link or button presentation along with ability to control horizontal alignment.

Example

<actuator>
    <action-id>Sapphire.Gallery.Actuators.DoubleTheNumber</action-id>
    <action>
        <id>Sapphire.Gallery.Actuators.DoubleTheNumber</id>
        <label>double the number</label>
    </action>
    <action-handler>
        <id>Sapphire.Gallery.Actuators.DoubleTheNumber</id>
        <action>Sapphire.Gallery.Actuators.DoubleTheNumber</action>
        <impl>ActuatorsGalleryDoubleTheNumberActionHandler</impl>
    </action-handler>
    <span>false</span>
    <horizontal-align>right</horizontal-align>
    <style>Sapphire.Actuator.Button</style>
</actuator>

Expression for Property Editor Label

Use the expression language when overriding the label in a property editor.

Example

<property-editor>
    <property>Name</property>
    <label>${ Type == "Business" ? "company" : "name" }</label>
</property-editor>

Part Services

Services can now be attached to parts. This enhancement allows extensibility for parts to be handled with the same API that adopters should already be familiar with from the model layer.

To attach a part service via the extension system, use Sapphire.Part context.

Example

<extension>
    <service>
        <id>Example.CustomLayoutPersistenceService</id>
        <context>Sapphire.Part</context>
        <type>org.eclipse.sapphire.ui.diagram.layout.DiagramLayoutPersistenceService</type>
        <factory>org.example.CustomLayoutPersistenceService$Factory</factory>
    </service>
</extension>

Services can also be attached to parts at the local level in sdef.

Example

<definition>
    <diagram-page>
        <service>            
            <implementation>CustomLayoutPersistenceService</implementation>
        </service>
    </diagram-page>
</definition>

AdapterService

The AdapterService provides means to extend the behavior of the adapt method in a given context.

For example, out of the box, Sapphire can adapt IModelElement to IProject assuming that the model is using an IFile for the underlying resource store. However, in some cases there may be no underlying IFile, so the default logic will no be able to find IProject. To solve this problem, we can introduce an adapter service to provide a custom method for adopting IModelElement to IProject.

Example

@GenerateImpl
@Service( impl = NewProjectFileOpAdapterService.class )

public interface INewProjectFileOp extends IModelElement
{
    ...
}
public class NewProjectFileOpAdapterService extends AdapterService
{
    @Override
    public <A> A adapt( Class<A> adapterType )
    {
        if( IProject.class == adapterType )
        {
            INewProjectFileOp op = context( INewProjectFileOp.class );
            IProject project = op.getProject().resolve();
            
            if( project != null )
            {
                return adapterType.cast( project );
            }
        }
        
        return null;
    }
}

DiagramLayoutPersistenceService

The DiagramLayoutPersistenceService is responsible for persisting layout of the diagram, such a location and size of nodes, connection bend points, etc.

Unlike other services, DiagramLayoutPersistenceService is not defined by methods that must be implemented, but rather by its expected behavior.

  1. During service initialization, the implementation is expected to load layout information and transfer it to diagram parts using API such as DiagramNodePart.setNodeBounds().
  2. After initialization, the implementation is expected to listen for changes to diagram parts and persist layout information. Persistence can happen immediately or be deferred until the save event is received.
  3. If implementation defers layout persistence until save, it is expected to implement dirty() method and to issue DirtyStateEvent when this state changes.

Example

The architecture sample provides a comprehensive example of a custom implementation that persists layout in the same file as data.

<diagram-page>
  <layout-persistence>custom</layout-persistence>
  <service>
    <implementation>ArchitectureDiagramLayoutPersistenceService</implementation>
  </service>
</diagram-page>

DragAndDropService

The DragAndDropService provides means to implement drag-n-drop behavior in a diagram editor.

Example

public class MapDragAndDropService extends DragAndDropService 
{
    @Override
    public boolean droppable( DropContext context ) 
    {
        return context.object() instanceof IFile;
    }
    
    @Override
    public void drop( DropContext context ) 
    {
        IFile file = (IFile) context.object();
        List locations = new ArrayList();
        
        InputStream in = null;
        
        try
        {
            in = file.getContents();
            final BufferedReader br = new BufferedReader( new InputStreamReader( in ) );
            
            for( String line = br.readLine(); line != null; line = br.readLine() )
            {
                if( line != null )
                {
                    line = line.trim();
                    
                    if( line.length() > 0 )
                    {
                        locations.add( line );
                    }
                }
            }
        }
        catch( CoreException e )
        {
            LoggingService.log( e );
        }
        catch( IOException e )
        {
            LoggingService.log( e );
        }
        finally
        {
            if( in != null )
            {
                try
                {
                    in.close();
                }
                catch( IOException e ) {}
            }
        }
        
        if( ! locations.isEmpty() )
        {
            SapphireDiagramEditorPagePart diagram = context( SapphireDiagramEditorPagePart.class );
            Map map = context( Map.class );
            
            Point initialDropPosition = context.position();
            
            int x = initialDropPosition.getX();
            int y = initialDropPosition.getY();
            
            for( String locationName : locations )
            {
                if( ! map.hasLocation( locationName ) )
                {
                    Location location = map.getLocations().insert();
                    location.setName( locationName );
                    
                    DiagramNodePart locationNodePart = diagram.getDiagramNodePart(location);
                    locationNodePart.setNodeBounds( x, y );
                    
                    x += 50;
                    y += 50;
                }
            }
        }
    }
}

The DragAndDropService is typically registered as part of diagram page definition in the sdef file.

<diagram-page>
    <service>
        <implementation>MapDropService</implementation>
    </service>
</diagram-page>

EqualityService

The EqualityService provides means to implement equals() and hashCode() methods when the context object doesn't support implementing these methods directly. One such context is model elements, where the framework does not rely on a particular implementation of these methods, but having these methods behave in a way consistent with semantics of the data being modeled can be useful for other purposes.

Example

public class ContactEqualityService extends EqualityService
{
    @Override
    public boolean doEquals( Object obj )
    {
        Contact c1 = context( Contact.class );
        Contact c2 = (Contact) obj;
        
        return equal( c1.getLastName().getText(), c2.getLastName().getText() ) &&
               equal( c1.getFirstName().getText(), c2.getFirstName().getText() );
    }
    
    @Override
    public int doHashCode()
    {
        Contact c = context( Contact.class );
        String lastName = c.getLastName().getText();
        String firstName = c.getFirstName().getText();
        
        return ( lastName == null ? 1 : lastName.hashCode() ) ^ ( firstName == null ? 1 : firstName.hashCode() );
    }
    
    private static boolean equal( Object obj1, Object obj2 )
    {
        if( obj1 == obj2 )
        {
            return true;
        }
        else if( obj1 != null && obj2 != null )
        {
            return obj1.equals( obj2 );
        }
        
        return false;
    }
}
@Services( { @Service( impl = ContactEqualityService.class ), ... } )

public interface Contact extends IModelElement
{
    ModelElementType TYPE = new ModelElementType( Contact.class );
    
    ...
}

ListSelectionService

The ListSelectionService functions as a conduit between the presentation layer and anything that may want to see or change the selection. The presentation layer pushes selection changes made by the user to ListSelectionService and at the same time listens for changes to selection coming from ListSelectionService.

An implementation of this service is provided with Sapphire. This service is not intended to be implemented by adopters.

Example

In this example, an action handler attaches a listener to the ListSelectionService to refresh action handler's enablement state when selection changes.


public class ExampleActionHandler extends SapphireActionHandler
{
    @Override
    public void init( SapphireAction action, ActionHandlerDef def )
    {
        super.init( action, def );
        
        final ListSelectionService selectionService = action.getPart().service( ListSelectionService.class );
        
        final Listener selectionListener = new Listener()
        {
            @Override
            public void handle( Event event )
            {
                refreshEnablementState();
            }
        };
        
        selectionService.attach( selectionListener );
        
        attach
        (
            new Listener()
            {
                @Override
                public void handle( Event event )
                {
                    if( event instanceof DisposeEvent )
                    {
                        selectionService.detach( selectionListener );
                    }
                }
            }
        );
    }
}

ValidationService at Element Level

The ValidationService provides means to check integrity constraints and to communicate problems to the user. In prior releases, the developer could only attach a ValidationService to properties. Now, a ValidationService can also be attached to elements.

Example

In this example, a validation service is used for detecting likely duplicate contacts. Two contacts are defined as duplicates of each other if e-mail and at least one of the phone numbers matches.

public class DuplicateContactValidationService extends ValidationService
{
    private Listener listener;
    
    @Override
    protected void init()
    {
        final ContactsDatabase contacts = context( Contact.class ).nearest( ContactsDatabase.class );
        
        if( contacts != null )
        {
            this.listener = new FilteredListener<PropertyContentEvent>()
            {
                @Override
                protected void handleTypedEvent( PropertyContentEvent event )
                {
                    broadcast();
                }
            };
            
            contacts.attach( this.listener, "Contacts/EMail" );
            contacts.attach( this.listener, "Contacts/PhoneNumbers/*" );
        }
    }
    
    @Override
    public Status validate() 
    {
        Contact contact = context( Contact.class );
        ContactsDatabase contacts = contact.nearest( ContactsDatabase.class );
        
        if( contacts != null )
        {
            String email = contact.getEMail().getContent();
            ModelElementList<PhoneNumber> numbers = contact.getPhoneNumbers();
            
            if( email != null && ! numbers.isEmpty() )
            {
                for( Contact x : contacts.getContacts() )
                {
                    if( x != contact && email.equals( x.getEMail().getContent() ) && ! x.getPhoneNumbers().isEmpty() )
                    {
                        for( PhoneNumber cn : numbers )
                        {
                            for( PhoneNumber xn : x.getPhoneNumbers() )
                            {
                                if( equal( cn.getAreaCode().getContent(), xn.getAreaCode().getContent() ) &&
                                    equal( cn.getLocalNumber().getContent(), xn.getLocalNumber().getContent() ) )
                                {
                                    String msg = "Likely the same contact as " + x.getName() + ".";
                                    return createWarningStatus( msg );
                                }
                            }
                        }
                    }
                }
            }
        }
        
        return createOkStatus();
    }
    
    @Override
    public void dispose()
    {
        ContactsDatabase contacts = context( Contact.class ).nearest( ContactsDatabase.class );
        
        if( contacts != null )
        {
            contacts.detach( this.listener, "Contacts/EMail" );
            contacts.detach( this.listener, "Contacts/PhoneNumbers/*" );
        }
    }
}
@Services( { @Service( impl = DuplicateContactValidationService.class ), ... } )

public interface Contact extends IModelElement
{
    ModelElementType TYPE = new ModelElementType( Contact.class );
    
    ...
}

IModelElement.disposed()

The new IModelElement.disposed() method provides a way to check if a model element has been disposed already.

Image Function

Returns the image associated with the context model element.

Examples

${ Image() }
${ Address.Image() }
${ BusinessExpense ? BusinessAccount.Image() : PersonalAccount.Image() }

Propagated Key Bindings

When processing keyboard events, the system now takes into account actions defined in part's hierarchy rather than just the local ones. For instance, an action defined at editor page level can be made accessible via its key binding even if the focus is on a property editor. This is done by specifying propagated key binding behavior as part of action definition.

Example

<action>
    <id>Sapphire.Gallery.Open.Homepage</id>
    <label>Sapphire Homepage</label>
    <tooltip>Open Sapphire Homepage (Ctrl+Alt+Shift+S)</tooltip>
    <image>Web.png</image>
    <description>Open Sapphire project homepage.</description>
    <key-binding>CONTROL+ALT+SHIFT+s</key-binding>
    <key-binding-behavior>propagated</key-binding-behavior>
    <context>Sapphire.EditorPage</context>
    <hint>
        <name>style</name>
        <value>image+text</value>
    </hint>
</action>

Action Tool Tips

By default, action label is used when a tool tip is necessary. If that's not appropriate, developer can now set the action tool tip explicitly.

Example

<action>
    <id>Sapphire.Gallery.Open.Homepage</id>
    <label>Sapphire Homepage</label>
    <tooltip>Open Sapphire Homepage (Ctrl+Alt+Shift+S)</tooltip>
    <image>Web.png</image>
    <description>Open Sapphire project homepage.</description>
    <key-binding>CONTROL+ALT+SHIFT+s</key-binding>
    <key-binding-behavior>propagated</key-binding-behavior>
    <context>Sapphire.EditorPage</context>
    <hint>
        <name>style</name>
        <value>image+text</value>
    </hint>
</action>