/****************************************************************************** * Copyright (c) 2016 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: * Konstantin Komissarchik - initial implementation and ongoing maintenance ******************************************************************************/ package org.eclipse.sapphire.ui.forms.swt.internal; import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gd; import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gdvalign; import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gdvindent; 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.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import org.eclipse.sapphire.Element; import org.eclipse.sapphire.ElementList; import org.eclipse.sapphire.ElementType; import org.eclipse.sapphire.Event; import org.eclipse.sapphire.FilteredListener; import org.eclipse.sapphire.ImageData; import org.eclipse.sapphire.ImageService; import org.eclipse.sapphire.Listener; import org.eclipse.sapphire.LocalizableText; import org.eclipse.sapphire.LoggingService; import org.eclipse.sapphire.PossibleValuesService; import org.eclipse.sapphire.PropertyContentEvent; import org.eclipse.sapphire.PropertyDef; import org.eclipse.sapphire.PropertyEvent; import org.eclipse.sapphire.PropertyValidationEvent; import org.eclipse.sapphire.Sapphire; import org.eclipse.sapphire.Suspension; import org.eclipse.sapphire.Text; import org.eclipse.sapphire.ValueProperty; import org.eclipse.sapphire.modeling.CapitalizationType; import org.eclipse.sapphire.modeling.localization.LocalizationService; import org.eclipse.sapphire.services.ValueImageService; import org.eclipse.sapphire.services.ValueLabelService; import org.eclipse.sapphire.ui.DelayedTasksExecutor; import org.eclipse.sapphire.ui.DelayedTasksExecutor.Task; import org.eclipse.sapphire.ui.assist.internal.PropertyEditorAssistDecorator; import org.eclipse.sapphire.ui.def.Orientation; import org.eclipse.sapphire.ui.forms.FormComponentPart; import org.eclipse.sapphire.ui.forms.PropertyEditorPart; import org.eclipse.sapphire.ui.forms.swt.ListPropertyEditorPresentation; import org.eclipse.sapphire.ui.forms.swt.PropertyEditorPresentation; import org.eclipse.sapphire.ui.forms.swt.SwtPresentation; import org.eclipse.sapphire.util.SetFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; /** * @author <a href="mailto:konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a> */ public final class CheckBoxGroupPropertyEditorPresentation extends ListPropertyEditorPresentation { @Text( "<empty>" ) private static LocalizableText emptyIndicator; static { LocalizableText.init( CheckBoxGroupPropertyEditorPresentation.class ); } private final Orientation orientation; private ValueProperty memberProperty; private PossibleValuesService possibleValuesService; private ValueLabelService valueLabelService; private ValueImageService valueImageService; private LocalizationService localizationService; private Listener serviceListener; private Composite checkBoxesComposite; public CheckBoxGroupPropertyEditorPresentation( final FormComponentPart part, final SwtPresentation parent, final Composite composite, final Orientation orientation ) { super( part, parent, composite ); this.orientation = orientation; } @Override protected void createContents( final Composite parent ) { final ElementList<?> list = property(); final ElementType memberType = list.definition().getType(); final SortedSet<PropertyDef> allMemberProperties = memberType.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(); } final Composite mainComposite = createMainComposite( parent ); mainComposite.setLayout( glspacing( glayout( 2, 0, 0 ), 2 ) ); final PropertyEditorAssistDecorator decorator = createDecorator( mainComposite ); decorator.control().setLayoutData( gdvindent( gdvalign( gd(), ( this.orientation == Orientation.HORIZONTAL ? SWT.CENTER : SWT.TOP ) ), ( this.orientation == Orientation.HORIZONTAL ? 0 : 4 ) ) ); this.checkBoxesComposite = new Composite( mainComposite, SWT.NONE ); this.checkBoxesComposite.setLayoutData( gdvalign( gd(), SWT.CENTER ) ); final RowLayout checkBoxesCompositeLayout = new RowLayout(); checkBoxesCompositeLayout.type = ( this.orientation == Orientation.HORIZONTAL ? SWT.HORIZONTAL : SWT.VERTICAL ); checkBoxesCompositeLayout.wrap = false; checkBoxesCompositeLayout.marginTop = 0; checkBoxesCompositeLayout.marginBottom = 0; checkBoxesCompositeLayout.marginLeft = 0; checkBoxesCompositeLayout.marginRight = 0; checkBoxesCompositeLayout.spacing = ( this.orientation == Orientation.HORIZONTAL ? 10 : 5 ); this.checkBoxesComposite.setLayout( checkBoxesCompositeLayout ); this.serviceListener = new Listener() { @Override public void handle( final Event event ) { refreshCheckBoxes(); } }; this.localizationService = part().definition().adapt( LocalizationService.class ); this.possibleValuesService = list.service( PossibleValuesService.class ); this.possibleValuesService.attach( this.serviceListener ); this.valueLabelService = this.memberProperty.service( ValueLabelService.class ); this.valueLabelService.attach( this.serviceListener ); this.valueImageService = this.memberProperty.service( ValueImageService.class ); this.valueImageService.attach( this.serviceListener ); final Listener modelListener = new FilteredListener<PropertyEvent>() { @Override protected void handleTypedEvent( final PropertyEvent event ) { if( event instanceof PropertyContentEvent || event instanceof PropertyValidationEvent ) { refreshCheckBoxes(); } } }; list.attach( modelListener, this.memberProperty.name() ); addOnDisposeOperation ( new Runnable() { @Override public void run() { CheckBoxGroupPropertyEditorPresentation.this.possibleValuesService.detach( CheckBoxGroupPropertyEditorPresentation.this.serviceListener ); CheckBoxGroupPropertyEditorPresentation.this.valueLabelService.detach( CheckBoxGroupPropertyEditorPresentation.this.serviceListener ); CheckBoxGroupPropertyEditorPresentation.this.valueImageService.detach( CheckBoxGroupPropertyEditorPresentation.this.serviceListener ); if( ! list.disposed() ) { list.detach( modelListener, CheckBoxGroupPropertyEditorPresentation.this.memberProperty.name() ); for( final Element element : list ) { final ImageService imageService = element.service( ImageService.class ); if( imageService != null ) { imageService.detach( CheckBoxGroupPropertyEditorPresentation.this.serviceListener ); } } } } } ); refreshCheckBoxes(); } private void refreshCheckBoxes() { final ElementList<?> list = property(); // Build a list of existing check boxes. final List<Button> checkboxes = new ArrayList<Button>(); for( final Control control : this.checkBoxesComposite.getChildren() ) { checkboxes.add( (Button) control ); } // Build a map of value to existing elements. final Map<String,LinkedList<Element>> valueToElements = new HashMap<String,LinkedList<Element>>(); for( final Element element : list ) { final String value = readMemberProperty( element ); LinkedList<Element> elements = valueToElements.get( value ); if( elements == null ) { elements = new LinkedList<Element>(); valueToElements.put( value, elements ); } elements.add( element ); } // Retrieve the set of possible values. Set<String> possibleValues; try { possibleValues = possibleValuesService.values(); } catch( Exception e ) { Sapphire.service( LoggingService.class ).log( e ); possibleValues = SetFactory.empty(); } // Create or rebase check boxes for all possible values and all elements. int position = 0; for( final String value : possibleValues ) { final LinkedList<Element> elements = valueToElements.get( value ); if( elements == null ) { createOrRebaseCheckBox( checkboxes, position, value, null ); position++; } else { for( final Element element : elements ) { createOrRebaseCheckBox( checkboxes, position, value, element ); position++; } valueToElements.remove( value ); } } for( final Iterator<Map.Entry<String,LinkedList<Element>>> itr = valueToElements.entrySet().iterator(); itr.hasNext(); ) { final Map.Entry<String,LinkedList<Element>> entry = itr.next(); final String value = entry.getKey(); if( value != null ) { for( final Element element : entry.getValue() ) { createOrRebaseCheckBox( checkboxes, position, value, element ); position++; } itr.remove(); } } for( final Map.Entry<String,LinkedList<Element>> entry : valueToElements.entrySet() ) { final String value = entry.getKey(); for( final Element element : entry.getValue() ) { createOrRebaseCheckBox( checkboxes, position, value, element ); position++; } } // Dispose of extra check boxes. for( int i = position, n = checkboxes.size(); i < n; i++ ) { checkboxes.get( i ).dispose(); } // Re-layout everything since the number of check boxes may have changed and their labels may have changed. layout(); } private void createOrRebaseCheckBox( final List<Button> checkboxes, final int position, final String value, final Element element ) { final Button checkbox; if( position < checkboxes.size() ) { checkbox = checkboxes.get( position ); } else { checkbox = createCheckBox(); } rebaseCheckBox( checkbox, value, element ); } private Button createCheckBox() { final Button checkbox = new Button( this.checkBoxesComposite, SWT.CHECK ); checkbox.addSelectionListener ( new SelectionAdapter() { @Override public void widgetSelected( final SelectionEvent event ) { final Task task = new Task() { @Override public void run() { if( checkbox.isDisposed() ) { return; } final ElementList<?> list = property(); final boolean selection = checkbox.getSelection(); Element e = (Element) checkbox.getData( "Element" ); if( e == null && selection == true ) { try( Suspension s = list.suspend() ) { e = list.insert(); writeMemberProperty( e, (String) checkbox.getData( "Value" ) ); } } else if( e != null && selection == false ) { list.remove( e ); } } }; DelayedTasksExecutor.schedule( task ); } } ); addControl( checkbox ); this.decorator.addEditorControl( checkbox ); return checkbox; } private void rebaseCheckBox( final Button checkbox, final String value, final Element element ) { checkbox.setData( "Value", value ); checkbox.setData( "Element", element ); checkbox.setSelection( element != null ); String text = null; if( value == null ) { text = emptyIndicator.text(); } else { try { text = this.valueLabelService.provide( value ); } catch( Exception e ) { Sapphire.service( LoggingService.class ).log( e ); } if( text == null ) { text = value; } else if( ! text.equals( value ) ) { text = this.localizationService.transform( text, CapitalizationType.FIRST_WORD_ONLY, false ); } } checkbox.setText( text ); ImageService elementImageService = (ImageService) checkbox.getData( "ImageService" ); if( elementImageService != null ) { elementImageService.detach( this.serviceListener ); } if( element != null ) { elementImageService = element.service( ImageService.class ); checkbox.setData( "ImageService", elementImageService ); if( elementImageService != null ) { elementImageService.attach( this.serviceListener ); } } ImageData image = null; if( elementImageService != null ) { image = elementImageService.image(); } else { try { image = this.valueImageService.provide( value ); } catch( Exception e ) { Sapphire.service( LoggingService.class ).log( e ); } } if( element == null ) { checkbox.setImage( resources().image( image ) ); } else { checkbox.setImage( resources().image( image, element.property( this.memberProperty ).validation().severity() ) ); } } private String readMemberProperty( final Element element ) { return element.property( this.memberProperty ).text(); } private void writeMemberProperty( final Element element, final String text ) { element.property( this.memberProperty ).write( text, true ); } @Override protected void handleFocusReceivedEvent() { for( final Control control : this.checkBoxesComposite.getChildren() ) { control.setFocus(); break; } } public static final class HorizontalFactory extends PossibleValuesListPresentationFactory { @Override protected boolean check( final PropertyEditorPart part ) { final String style = part.definition().getStyle().content(); if( style != null && ( style.equals( "Sapphire.PropertyEditor.CheckBoxGroup" ) || style.equals( "Sapphire.PropertyEditor.CheckBoxGroup.Horizontal" ) ) ) { return super.check( part ); } return false; } @Override public PropertyEditorPresentation create( final PropertyEditorPart part, final SwtPresentation parent, final Composite composite ) { if( check( part ) ) { return new CheckBoxGroupPropertyEditorPresentation( part, parent, composite, Orientation.HORIZONTAL ); } return null; } } public static final class VerticalFactory extends PossibleValuesListPresentationFactory { @Override protected boolean check( final PropertyEditorPart part ) { final String style = part.definition().getStyle().content(); if( style != null && style.equals( "Sapphire.PropertyEditor.CheckBoxGroup.Vertical" ) ) { return super.check( part ); } return false; } @Override public PropertyEditorPresentation create( final PropertyEditorPart part, final SwtPresentation parent, final Composite composite ) { if( check( part ) ) { return new CheckBoxGroupPropertyEditorPresentation( part, parent, composite, Orientation.VERTICAL ); } return null; } } }