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.
Customize how diagram layout is persisted by implementing DiagramLayoutPersistenceService or take advantage of one of the provided implementations:
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>
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 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>
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.
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>
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.
Use drag-n-drop to arrange the elements in the content outline and in table property editors.
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
<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
<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
<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>
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>
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>
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>
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;
}
}
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.
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>
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>
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 );
...
}
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 );
}
}
}
);
}
}
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 );
...
}
The new IModelElement.disposed() method provides a way to check if a model element has been disposed already.
Returns the image associated with the context model element.
Examples
${ Image() }
${ Address.Image() }
${ BusinessExpense ? BusinessAccount.Image() : PersonalAccount.Image() }
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>
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>