/******************************************************************************
* Copyright (c) 2014 Oracle
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Ling Hao - initial implementation and ongoing maintenance
* Konstantin Komissarchik - [343677] Element property validation is not surfaced by with directive
* Konstantin Komissarchik - [338857] List property editor doesn't disable add action button when the list property is disabled
* Konstantin Komissarchik - [392773] IllegalArgumentException while selecting outline node in gallery
******************************************************************************/
package org.eclipse.sapphire.ui.forms.swt;
import static org.eclipse.sapphire.ui.SapphireActionSystem.ACTION_ASSIST;
import static org.eclipse.sapphire.ui.SapphireActionSystem.ACTION_DELETE;
import static org.eclipse.sapphire.ui.SapphireActionSystem.ACTION_JUMP;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gd;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gdfill;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gdhfill;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gdhindent;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gdhspan;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gdvalign;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gdvfill;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gdvindent;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gdwhint;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.glayout;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.glspacing;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
import org.eclipse.sapphire.Element;
import org.eclipse.sapphire.ElementList;
import org.eclipse.sapphire.ListProperty;
import org.eclipse.sapphire.Property;
import org.eclipse.sapphire.PropertyBinding;
import org.eclipse.sapphire.PropertyContentEvent;
import org.eclipse.sapphire.PropertyDef;
import org.eclipse.sapphire.Value;
import org.eclipse.sapphire.ValueProperty;
import org.eclipse.sapphire.modeling.CapitalizationType;
import org.eclipse.sapphire.modeling.xml.XmlElement;
import org.eclipse.sapphire.modeling.xml.XmlResource;
import org.eclipse.sapphire.modeling.xml.XmlValueBindingImpl;
import org.eclipse.sapphire.ui.Presentation;
import org.eclipse.sapphire.ui.SapphireAction;
import org.eclipse.sapphire.ui.SapphireActionGroup;
import org.eclipse.sapphire.ui.SapphireActionHandler;
import org.eclipse.sapphire.ui.SapphireActionHandlerFilter;
import org.eclipse.sapphire.ui.SapphireActionSystem;
import org.eclipse.sapphire.ui.assist.internal.PropertyEditorAssistDecorator;
import org.eclipse.sapphire.ui.forms.FormComponentPart;
import org.eclipse.sapphire.ui.forms.PropertyEditorPart;
import org.eclipse.sapphire.ui.forms.swt.internal.TextOverlayPainter;
import org.eclipse.sapphire.ui.forms.swt.internal.text.SapphireFormText;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.ui.forms.events.HyperlinkAdapter;
import org.eclipse.ui.forms.events.HyperlinkEvent;
/**
* @author <a href="mailto:ling.hao@oracle.com">Ling Hao</a>
* @author <a href="mailto:konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a>
*/
public final class CompactListPropertyEditorPresentation extends ListPropertyEditorPresentation
{
public static final String DATA_SELECTION_PROVIDER = "selection.provider";
private Runnable refreshOperation;
ValueProperty memberProperty;
private Composite mainComposite;
private Composite textComposite;
private List<TextBinding> textBindings = new ArrayList<TextBinding>();
private SapphireFormText addText;
private HyperlinkAdapter addTextHyperlinkAdapter;
public CompactListPropertyEditorPresentation( final FormComponentPart part, final SwtPresentation parent, final Composite composite )
{
super( part, parent, composite );
}
@Override
protected void createContents( final Composite parent )
{
final Property property = property();
// TODO support readonly
//final boolean isReadOnly = ( property.isReadOnly() || part.getRenderingHint( HINT_READ_ONLY, false ) );
final SortedSet<PropertyDef> allMemberProperties = property.definition().getType().properties();
if( allMemberProperties.size() == 1 )
{
final PropertyDef prop = allMemberProperties.first();
if( prop instanceof ValueProperty )
{
this.memberProperty = (ValueProperty) prop;
}
else
{
throw new IllegalStateException();
}
}
else
{
throw new IllegalStateException();
}
this.mainComposite = createMainComposite( parent );
this.mainComposite.setLayout( glayout( 2, 0, 0, 2, 2 ) );
addControls( 1 );
this.refreshOperation = new Runnable()
{
boolean running = false;
public void run()
{
if( CompactListPropertyEditorPresentation.this.mainComposite.isDisposed() )
{
return;
}
if( this.running == true )
{
return;
}
this.running = true;
try
{
CompactListPropertyEditorPresentation.this.refreshControls();
}
finally
{
this.running = false;
}
}
};
this.binding = new AbstractBinding( this, this.mainComposite )
{
@Override
protected void doUpdateModel()
{
// Don't do anything here. Changes to the model are pushed at cell-level.
}
@Override
protected void doUpdateTarget()
{
// Changes are mostly synchronized at cell-level. This is only here to
// catch the case where a full page refresh is performed perhaps because user
// has visited the source page.
CompactListPropertyEditorPresentation.this.refreshOperation.run();
}
};
}
private void addControls( final int count )
{
final PropertyEditorPart part = part();
final ElementList<?> list = list();
// first remove everything
if (this.addText != null && this.addTextHyperlinkAdapter != null) {
this.addText.removeHyperlinkListener(this.addTextHyperlinkAdapter);
}
for (int i = 0; i < this.textBindings.size(); i++) {
this.textBindings.get(i).removeListener();
}
for (Control child : this.mainComposite.getChildren()) {
child.dispose();
}
this.textBindings.clear();
this.textComposite = new Composite( this.mainComposite, SWT.NONE );
this.textComposite.setLayoutData( gdhspan( gdfill(), 2 ) );
this.textComposite.setLayout( glspacing( glayout( 3, 0, 0 ), 2, 4 ) );
addControl( this.textComposite );
// add back text controls and add link
for (int i = 0; i < count; i++) {
final ProxyResource resource = new ProxyResource();
final Element proxyElement = this.memberProperty.getModelElementType().instantiate(list, resource);
resource.init(proxyElement, this.memberProperty);
final PropertyEditorPart editor = part.getChildPropertyEditor( proxyElement, this.memberProperty );
PropertyEditorAssistDecorator decorator = addDecorator(editor);
Text text = new Text(this.textComposite, SWT.BORDER);
text.setLayoutData( gdhindent( gdwhint( gdhfill(), 150 ), 0 ) );
TextBinding binding = new TextBinding(text, resource);
binding.setDecorator(decorator);
this.textBindings.add(binding);
addControl( text );
addToolbar(binding, editor);
decorator.addEditorControl(text);
decorator.addEditorControl(binding.getToolbar());
}
this.addText = new SapphireFormText( this.mainComposite, SWT.NONE );
this.addText.setLayoutData( gdhindent( gdvalign( gdhfill(), SWT.CENTER ), 10 ) );
addControl(this.addText);
final StringBuilder buf = new StringBuilder();
buf.append( "<form><p vspace=\"false\"><a href=\"action\" nowrap=\"true\">" );
buf.append( "Add " );
final ListProperty listProperty = (ListProperty) property().definition();
buf.append( listProperty.getType().getLabel(false, CapitalizationType.NO_CAPS, false/*includeMnemonic*/) );
buf.append( "</a></p></form>" );
this.addText.setText( buf.toString(), true, false );
if (this.addTextHyperlinkAdapter == null) {
this.addTextHyperlinkAdapter = new HyperlinkAdapter() {
@Override
public void linkActivated( final HyperlinkEvent event )
{
addActivated();
refreshDeleteActions();
}
};
}
this.addText.addHyperlinkListener(this.addTextHyperlinkAdapter);
}
private void addToolbar(final TextBinding binding, final PropertyEditorPart editor) {
final PropertyEditorPart part = part();
final SapphireActionGroup parentActions = new SapphireActionGroup(part, part.getActionContext());
final SapphireAction deleteAction = parentActions.getAction( ACTION_DELETE );
deleteAction.setGroup(null);
final DeleteActionHandler deleteActionHandler = new DeleteActionHandler(binding);
deleteActionHandler.init( deleteAction, null );
deleteAction.addHandler( deleteActionHandler );
final SapphireActionGroup actions = editor.getActions();
actions.addAction(deleteAction);
final SapphireActionHandler jumpActionHandler = actions.getAction( ACTION_JUMP ).getFirstActiveHandler();
addJumpOverlay(jumpActionHandler, binding);
final SapphireActionHandlerFilter assistFilter = SapphireActionSystem.createFilterByActionId( ACTION_ASSIST );
final SapphireActionHandlerFilter jumpFilter = SapphireActionSystem.createFilterByActionId( ACTION_JUMP );
actions.addFilter(assistFilter);
actions.addFilter(jumpFilter);
final SapphireActionPresentationManager actionPresentationManager = new SapphireActionPresentationManager( this, actions );
final SapphireToolBarActionPresentation toolBarActionsPresentation = new SapphireToolBarActionPresentation( actionPresentationManager );
final SapphireKeyboardActionPresentation actionPresentationKeyboard = new SapphireKeyboardActionPresentation( actionPresentationManager );
binding.setActionPresentationKeyboard(actionPresentationKeyboard);
final ToolBar toolbar = new ToolBar( this.textComposite, SWT.FLAT | SWT.HORIZONTAL );
toolbar.setLayoutData( gdhindent( gdvfill(), 2) );
toolBarActionsPresentation.setToolBar( toolbar );
toolBarActionsPresentation.render();
addControl(toolbar);
toolbar.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
deleteAction.removeHandler(deleteActionHandler);
actionPresentationKeyboard.dispose();
}
});
binding.setToolbar(toolbar);
binding.setDeleteActionHandler(deleteActionHandler);
actions.removeFilter(assistFilter);
actions.removeFilter(jumpFilter);
}
private void addJumpOverlay(final SapphireActionHandler jumpActionHandler, final TextBinding binding) {
final TextOverlayPainter.Controller textOverlayPainterController;
if( jumpActionHandler != null )
{
textOverlayPainterController = new TextOverlayPainter.Controller()
{
@Override
public boolean isHyperlinkEnabled()
{
return jumpActionHandler.isEnabled();
}
@Override
public void handleHyperlinkEvent()
{
jumpActionHandler.execute( CompactListPropertyEditorPresentation.this );
}
@Override
public String overlay()
{
ProxyResource resource = binding.getResource();
Element element = resource.getModelElement();
return element == null ? null : element.property( resource.getValueProperty() ).getDefaultText();
}
};
}
else
{
textOverlayPainterController = new TextOverlayPainter.Controller()
{
@Override
public String overlay()
{
ProxyResource resource = binding.getResource();
Element element = resource.getModelElement();
return element == null ? null : element.property( resource.getValueProperty() ).getDefaultText();
}
};
}
TextOverlayPainter.install( binding.getText(), textOverlayPainterController );
}
private PropertyEditorAssistDecorator addDecorator(final PropertyEditorPart editor) {
final PropertyEditorAssistDecorator decorator = new PropertyEditorAssistDecorator(editor, this.textComposite);
decorator.control().setLayoutData( gdvindent( gdvalign( gd(), SWT.TOP ), 2 ) );
return decorator;
}
private void addActivated() {
ProxyResource resource = new ProxyResource();
final Element proxyElement = this.memberProperty.getModelElementType().instantiate(list(), resource);
resource.init(proxyElement, this.memberProperty);
PropertyEditorPart editor = part().getChildPropertyEditor( proxyElement, this.memberProperty );
PropertyEditorAssistDecorator decorator = addDecorator(editor);
Text text = new Text(this.textComposite, SWT.BORDER);
text.setLayoutData( gdwhint( gdhfill(), 150 ) );
TextBinding binding = new TextBinding(text, resource);
binding.setDecorator(decorator);
this.textBindings.add(binding);
addControl(text);
addToolbar(binding, editor);
decorator.addEditorControl(text);
decorator.addEditorControl(binding.getToolbar());
layout();
text.setFocus();
}
private void refreshControls() {
if (!needsRefresh()) {
refreshDeleteActions();
return;
}
ElementList<?> list = list();
if (list.size() != this.textBindings.size()) {
this.addControls(Math.max(1, list.size()));
layout();
}
for (int i = 0; i < list.size(); i++) {
Element elem = list.get(i);
this.textBindings.get(i).refreshModelElement(elem);
}
if (list.size() == 0) {
this.textBindings.get(0).refreshModelElement(null);
}
refreshDeleteActions();
}
private void refreshDeleteActions() {
final int size = this.textBindings.size();
for (int i = 0; i < size ; i++) {
TextBinding binding = this.textBindings.get(i);
if (i == 0 && size == 1) {
binding.getDeleteActionHandler().refreshEnablement();
} else {
binding.getDeleteActionHandler().setEnabled(true);
}
}
}
private boolean needsRefresh() {
for (TextBinding binding : this.textBindings) {
if (binding.isModifying()) {
return false;
}
}
return true;
}
void deleteBinding(TextBinding binding) {
Element elem = binding.getModelElement();
Text text = binding.getText();
ToolBar toolbar = binding.getToolbar();
PropertyEditorAssistDecorator decorator = binding.getDecorator();
binding.setModifying(true);
if (elem != null) {
list().remove(elem);
}
binding.setModifying(false);
if (this.textBindings.size() > 1) {
binding.removeListener();
this.textBindings.remove(binding);
text.dispose();
toolbar.dispose();
Control control = decorator.control();
decorator.removeEditorControl(control);
control.dispose();
layout();
} else {
binding.refreshModelElement(null);
}
refreshDeleteActions();
}
public void insertEmpty(final TextBinding binding) {
for (TextBinding b : this.textBindings) {
if (b == binding) {
return;
}
if (b.getModelElement() == null) {
final Element newElement = list().insert();
b.setModelElement(newElement);
}
}
}
public static final boolean equals( Object o1, Object o2 ) {
boolean objectsAreEqual = false;
if (o1 == o2) {
objectsAreEqual = true;
} else if (o1 != null && o2 != null) {
objectsAreEqual = o1.equals(o2);
}
return objectsAreEqual;
}
public final PropertyEditorAssistDecorator getDecorator()
{
return this.decorator;
}
public final void setDecorator( final PropertyEditorAssistDecorator decorator )
{
this.decorator = decorator;
}
@Override
protected void handlePropertyChangedEvent()
{
super.handlePropertyChangedEvent();
CompactListPropertyEditorPresentation.this.refreshOperation.run();
}
@Override
protected void handleChildPropertyEvent( final PropertyContentEvent event )
{
super.handleChildPropertyEvent( event );
this.refreshOperation.run();
}
@Override
protected void handleFocusReceivedEvent()
{
this.textBindings.get(0).getText().setFocus();
}
private final class DeleteActionHandler extends SapphireActionHandler {
TextBinding binding;
public DeleteActionHandler(TextBinding binding) {
this.binding = binding;
}
@Override
protected Object run(Presentation context) {
CompactListPropertyEditorPresentation.this.deleteBinding(this.binding);
return null;
}
public final void refreshEnablement() {
setEnabled(this.binding.getModelElement() != null);
}
}
private final class ProxyResource extends XmlResource
{
private Element proxyElement;
private Element actualElement;
private ValueProperty actualProperty;
private String value;
public ProxyResource() {
super(part().getLocalModelElement().resource());
}
public void init(Element proxyElement, ValueProperty actualProperty) {
this.proxyElement = proxyElement;
this.actualProperty = actualProperty;
}
@Override
protected PropertyBinding createBinding(Property property) {
if (property instanceof Value) {
return new ProxyBinding();
}
return null;
}
public void setModelElement(Element element) {
this.actualElement = element;
this.value = element != null ? element.property(this.actualProperty).text() : null;
this.proxyElement.refresh();
}
public Element getModelElement() {
return this.actualElement;
}
public ValueProperty getValueProperty() {
return this.actualProperty;
}
private Element getActualElement(boolean create) {
if (create && this.actualElement == null) {
final Element element = list().insert();
setModelElement(element);
}
return this.actualElement;
}
private final class ProxyBinding extends XmlValueBindingImpl {
@Override
public String read() {
return ProxyResource.this.value;
}
@Override
public void write(String value) {
ProxyResource.this.value = value;
final Element element = getActualElement(true/*create*/);
final ValueProperty property = getValueProperty();
element.property( property ).write( value, true );
}
}
@Override
public XmlElement getXmlElement(boolean createIfNecessary) {
final Element element = getActualElement(true);
if (element != null) {
return ((XmlResource)element.resource()).getXmlElement();
}
return null;
}
}
private final class TextBinding implements ModifyListener
{
private Text text;
private ProxyResource resource;
private ToolBar toolbar;
private CompactListPropertyEditorPresentation.DeleteActionHandler deleteActionHandler;
private PropertyEditorAssistDecorator decorator;
private SapphireKeyboardActionPresentation actionPresentationKeyboard;
private boolean modifying = false;
public TextBinding(Text text, ProxyResource resource) {
this.text = text;
this.text.addModifyListener(this);
this.resource = resource;
}
public void removeListener() {
this.text.removeModifyListener(this);
this.actionPresentationKeyboard.dispose();
}
public Text getText() {
return this.text;
}
public ProxyResource getResource() {
return this.resource;
}
public void refreshModelElement(Element element) {
setModelElement(element);
String value = element != null ? element.property(this.resource.getValueProperty()).text() : null;
value = value == null ? "" : value;
if (!CompactListPropertyEditorPresentation.equals(value, this.text.getText())) {
this.text.setText(value);
}
}
public void setModelElement(Element element) {
this.resource.setModelElement(element);
}
public Element getModelElement() {
return this.resource.getModelElement();
}
public boolean isModifying() {
return this.modifying;
}
public void setModifying(boolean modifying) {
this.modifying = modifying;
}
public ToolBar getToolbar() {
return this.toolbar;
}
public void setToolbar(ToolBar toolbar) {
this.toolbar = toolbar;
}
public void setActionPresentationKeyboard(SapphireKeyboardActionPresentation actionPresentationKeyboard) {
this.actionPresentationKeyboard = actionPresentationKeyboard;
this.actionPresentationKeyboard.attach(this.text);
this.actionPresentationKeyboard.render();
}
public CompactListPropertyEditorPresentation.DeleteActionHandler getDeleteActionHandler() {
return this.deleteActionHandler;
}
public void setDeleteActionHandler(
CompactListPropertyEditorPresentation.DeleteActionHandler deleteActionHandler) {
this.deleteActionHandler = deleteActionHandler;
}
public PropertyEditorAssistDecorator getDecorator() {
return this.decorator;
}
public void setDecorator(PropertyEditorAssistDecorator decorator) {
this.decorator = decorator;
}
public void modifyText(ModifyEvent e) {
if( ! this.text.isDisposed() && ( this.text.getStyle() & SWT.READ_ONLY ) == 0 )
{
Element element = this.resource.getModelElement();
final String value = this.text.getText();
if (value.length() == 0 && e.getSource().equals(this.text) && element == null) {
// do nothing..
} else {
this.modifying = true;
boolean createNew = false;
if (element == null) {
// new element may not be the last one - insert empty strings
insertEmpty(this);
final Element newElement = list().insert();
setModelElement(newElement);
createNew = true;
}
this.resource.element().property( this.resource.getValueProperty() ).write( value, true );
if (createNew) {
this.text.setSelection(value.length(), value.length());
}
this.modifying = false;
}
}
}
}
public static final class Factory extends PropertyEditorPresentationFactory
{
@Override
public PropertyEditorPresentation create( final PropertyEditorPart part, final SwtPresentation parent, final Composite composite )
{
if( part.property().definition() instanceof ListProperty )
{
return new CompactListPropertyEditorPresentation( part, parent, composite );
}
return null;
}
}
}