/****************************************************************************** * Copyright (c) 2016 Oracle, Accenture and Modelity Technologies * 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 * Kamesh Sampath - [354199] Support content proposals in text field property editor * Roded Bahat - [376198] Vertically align actions for @LongString property editors ******************************************************************************/ package org.eclipse.sapphire.ui.forms.swt; import static org.eclipse.sapphire.ui.SapphireActionSystem.ACTION_ASSIST; import static org.eclipse.sapphire.ui.SapphireActionSystem.ACTION_BROWSE; import static org.eclipse.sapphire.ui.SapphireActionSystem.ACTION_JUMP; import static org.eclipse.sapphire.ui.SapphireActionSystem.createFilterByActionId; import static org.eclipse.sapphire.ui.forms.PropertyEditorPart.DATA_BINDING; import static org.eclipse.sapphire.ui.forms.PropertyEditorPart.RELATED_CONTROLS; 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.glayout; import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.glspacing; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.eclipse.jface.bindings.keys.KeyStroke; import org.eclipse.jface.bindings.keys.ParseException; import org.eclipse.jface.fieldassist.ContentProposalAdapter; import org.eclipse.jface.fieldassist.IContentProposal; import org.eclipse.jface.fieldassist.IContentProposalProvider; import org.eclipse.jface.fieldassist.TextContentAdapter; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.sapphire.Event; import org.eclipse.sapphire.FilteredListener; import org.eclipse.sapphire.Length; import org.eclipse.sapphire.Listener; import org.eclipse.sapphire.LoggingService; import org.eclipse.sapphire.Sapphire; import org.eclipse.sapphire.Value; import org.eclipse.sapphire.ValueProperty; import org.eclipse.sapphire.modeling.annotations.LongString; import org.eclipse.sapphire.modeling.annotations.SensitiveData; import org.eclipse.sapphire.services.ContentProposal; import org.eclipse.sapphire.services.ContentProposalService; import org.eclipse.sapphire.ui.SapphireAction; import org.eclipse.sapphire.ui.SapphireActionGroup; import org.eclipse.sapphire.ui.SapphireActionHandler; import org.eclipse.sapphire.ui.SapphireActionHandler.PostExecuteEvent; import org.eclipse.sapphire.ui.SapphireActionHandlerFilter; import org.eclipse.sapphire.ui.SapphirePart; import org.eclipse.sapphire.ui.TextSelectionService; import org.eclipse.sapphire.ui.TextSelectionService.TextSelectionEvent; import org.eclipse.sapphire.ui.assist.internal.PropertyEditorAssistDecorator; import org.eclipse.sapphire.ui.forms.FormComponentPart; import org.eclipse.sapphire.ui.forms.JumpActionHandler; import org.eclipse.sapphire.ui.forms.PropertyEditorDef; import org.eclipse.sapphire.ui.forms.PropertyEditorPart; import org.eclipse.sapphire.ui.forms.swt.internal.TextFieldBinding; import org.eclipse.sapphire.ui.forms.swt.internal.TextOverlayPainter; import org.eclipse.sapphire.ui.listeners.ValuePropertyEditorListener; 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.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.ToolBar; /** * @author <a href="mailto:konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a> * @author <a href="mailto:kamesh.sampath@accenture.com">Kamesh Sampath</a> * @author <a href="mailto:rodedb@gmail.com">Roded Bahat</a> */ public class TextFieldPropertyEditorPresentation extends ValuePropertyEditorPresentation { private Text textField; private static final String CONTENT_ASSIST_KEY_STROKE_STRING = "Ctrl+Space"; private static final KeyStroke CONTENT_ASSIST_KEY_STROKE; static { KeyStroke keyStroke = null; try { keyStroke = KeyStroke.getInstance( CONTENT_ASSIST_KEY_STROKE_STRING ); } catch( ParseException e ) { Sapphire.service( LoggingService.class ).log( e ); } CONTENT_ASSIST_KEY_STROKE = keyStroke; } public TextFieldPropertyEditorPresentation( final FormComponentPart part, final SwtPresentation parent, final Composite composite ) { super( part, parent, composite ); } @Override protected void createContents( final Composite parent ) { createContents( parent, false ); } protected Control createContents( final Composite parent, final boolean suppressBrowseAction ) { final PropertyEditorPart part = part(); final Value<?> property = (Value<?>) part.property(); final ValueProperty pdef = property.definition(); final boolean isLongString = pdef.hasAnnotation( LongString.class ); final boolean isDeprecated = pdef.hasAnnotation( Deprecated.class ); final boolean isReadOnly = ( pdef.isReadOnly() || part.getRenderingHint( PropertyEditorDef.HINT_READ_ONLY, false ) ); final boolean isSensitiveData = pdef.hasAnnotation( SensitiveData.class ); final SapphireActionGroup actions = getActions(); final SapphireActionHandler jumpActionHandler = actions.getAction( ACTION_JUMP ).getFirstActiveHandler(); final SapphireToolBarActionPresentation toolBarActionsPresentation = new SapphireToolBarActionPresentation( getActionPresentationManager() ); toolBarActionsPresentation.addFilter( createFilterByActionId( ACTION_ASSIST ) ); toolBarActionsPresentation.addFilter( createFilterByActionId( ACTION_JUMP ) ); actions.addFilter ( new SapphireActionHandlerFilter() { @Override public boolean check( final SapphireActionHandler handler ) { final String actionId = handler.getAction().getId(); if( actionId.equals( ACTION_BROWSE ) && ( isReadOnly || suppressBrowseAction ) ) { return false; } return true; } } ); final boolean isActionsToolBarNeeded = toolBarActionsPresentation.hasActions(); final boolean isBrowseOnly = part.getRenderingHint( PropertyEditorDef.HINT_BROWSE_ONLY, false ); final Composite textFieldParent = createMainComposite ( parent, new CreateMainCompositeDelegate( part ) { @Override public boolean canScaleVertically() { return isLongString; } } ); addControl( textFieldParent ); int textFieldParentColumns = 1; if( isActionsToolBarNeeded ) textFieldParentColumns++; if( isDeprecated ) textFieldParentColumns++; textFieldParent.setLayout( glayout( textFieldParentColumns, 0, 0, 0, 0 ) ); final Composite nestedComposite = new Composite( textFieldParent, SWT.NONE ); nestedComposite.setLayoutData( isLongString ? gdfill() : gdvalign( gdhfill(), SWT.CENTER ) ); nestedComposite.setLayout( glspacing( glayout( 2, 0, 0 ), 2 ) ); addControl( nestedComposite ); final PropertyEditorAssistDecorator decorator = createDecorator( nestedComposite ); decorator.control().setLayoutData( gdvalign( gd(), SWT.TOP ) ); decorator.addEditorControl( nestedComposite ); final int style = SWT.BORDER | ( isLongString ? SWT.MULTI | SWT.WRAP | SWT.V_SCROLL : SWT.NONE ) | ( ( isReadOnly || isBrowseOnly ) ? SWT.READ_ONLY : SWT.NONE ) | ( isSensitiveData ? SWT.PASSWORD : SWT.NONE ); this.textField = new Text( nestedComposite, style ); this.textField.setLayoutData( gdfill() ); decorator.addEditorControl( this.textField, true ); TextOverlayPainter.install( this.textField, property, (JumpActionHandler) jumpActionHandler, this ); if( isBrowseOnly || isReadOnly ) { final Color bgcolor = new Color( this.textField.getDisplay(), 245, 245, 245 ); this.textField.setBackground( bgcolor ); this.textField.addDisposeListener ( new DisposeListener() { public void widgetDisposed( final DisposeEvent event ) { bgcolor.dispose(); } } ); } attachAccessibleName( this.textField ); final List<Control> relatedControls = new ArrayList<Control>(); this.textField.setData( RELATED_CONTROLS, relatedControls ); final Listener actionHandlerListener = new Listener() { @Override public void handle( final Event event ) { if( event instanceof PostExecuteEvent ) { if( ! TextFieldPropertyEditorPresentation.this.textField.isDisposed() ) { TextFieldPropertyEditorPresentation.this.textField.setFocus(); } } } }; for( SapphireAction action : actions.getActions() ) { if( ! action.getId().equals( ACTION_ASSIST ) ) { for( SapphireActionHandler handler : action.getActiveHandlers() ) { handler.attach( actionHandlerListener ); } } } if( isActionsToolBarNeeded ) { final int alignment = ( isLongString ? SWT.VERTICAL : SWT.HORIZONTAL ); final ToolBar toolbar = new ToolBar( textFieldParent, SWT.FLAT | alignment ); toolbar.setLayoutData( gdvfill() ); toolBarActionsPresentation.setToolBar( toolbar ); toolBarActionsPresentation.render(); addControl( toolbar ); decorator.addEditorControl( toolbar ); relatedControls.add( toolbar ); } final ContentProposalService contentProposalService = property.service( ContentProposalService.class ); if( contentProposalService != null ) { final ContentProposalProvider contentProposalProvider = new ContentProposalProvider( contentProposalService, part ); final ContentProposalAdapter contentProposalAdapter = new ContentProposalAdapter( this.textField, new TextContentAdapter(), contentProposalProvider, CONTENT_ASSIST_KEY_STROKE, null ); contentProposalAdapter.setPropagateKeys( true ); contentProposalAdapter.setLabelProvider( new ContentProposalLabelProvider() ); contentProposalAdapter.setProposalAcceptanceStyle( ContentProposalAdapter.PROPOSAL_REPLACE ); } if( isDeprecated ) { final Control deprecationMarker = createDeprecationMarker( textFieldParent ); deprecationMarker.setLayoutData( gd() ); } this.binding = new TextFieldBinding( this, this.textField ); this.textField.setData( DATA_BINDING, this.binding ); addControl( this.textField ); final Length lengthAnnotation = pdef.getAnnotation( Length.class ); if( lengthAnnotation != null && lengthAnnotation.max() < Integer.MAX_VALUE ) { final boolean span = part().getSpanBothColumns(); if( ! span ) { new Label( parent, SWT.NONE ); } final TextCapacityFeedback textCapacityFeedback = new TextCapacityFeedback( parent, this.textField, lengthAnnotation.max() ); textCapacityFeedback.setLayoutData( gdhspan( gdhindent( gdhfill(), 9 ), span ? 2 : 1 ) ); addControl( textCapacityFeedback ); decorator.addEditorControl( textCapacityFeedback ); } // Integrate with TextSelectionService final TextSelectionService textSelectionService = part.service( TextSelectionService.class ); final TextSelectionService.Range initialTextSelectionRange = textSelectionService.selection(); this.textField.setSelection( initialTextSelectionRange.start(), initialTextSelectionRange.end() ); final org.eclipse.swt.widgets.Listener textFieldSelectionListener = new org.eclipse.swt.widgets.Listener() { @Override public void handleEvent( final org.eclipse.swt.widgets.Event event ) { final Point selection = TextFieldPropertyEditorPresentation.this.textField.getSelection(); textSelectionService.select( selection.x, selection.y ); } }; this.textField.addListener( SWT.MouseUp, textFieldSelectionListener ); this.textField.addListener( SWT.KeyUp, textFieldSelectionListener ); final Listener textSelectionServiceListener = new FilteredListener<TextSelectionService.TextSelectionEvent>() { @Override protected void handleTypedEvent( final TextSelectionEvent event ) { final Point current = TextFieldPropertyEditorPresentation.this.textField.getSelection(); final TextSelectionService.Range selection = event.after(); if( ! ( current.x == selection.start() && current.y == selection.end() ) ) { TextFieldPropertyEditorPresentation.this.textField.setSelection( selection.start(), selection.end() ); } } }; textSelectionService.attach( textSelectionServiceListener ); this.textField.addDisposeListener ( new DisposeListener() { public void widgetDisposed( final DisposeEvent event ) { textSelectionService.detach( textSelectionServiceListener ); } } ); // Hookup property editor listeners. final List<Class<?>> listenerClasses = part.getRenderingHint( PropertyEditorDef.HINT_LISTENERS, Collections.<Class<?>>emptyList() ); if( ! listenerClasses.isEmpty() ) { final List<ValuePropertyEditorListener> listeners = new ArrayList<ValuePropertyEditorListener>(); for( Class<?> cl : listenerClasses ) { try { final ValuePropertyEditorListener listener = (ValuePropertyEditorListener) cl.newInstance(); listener.initialize( this ); listeners.add( listener ); } catch( Exception e ) { Sapphire.service( LoggingService.class ).log( e ); } } if( ! listeners.isEmpty() ) { this.textField.addModifyListener ( new ModifyListener() { public void modifyText( final ModifyEvent event ) { for( ValuePropertyEditorListener listener : listeners ) { try { listener.handleValueChanged(); } catch( Exception e ) { Sapphire.service( LoggingService.class ).log( e ); } } } } ); } } return this.textField; } @Override protected boolean canScaleVertically() { return property().definition().hasAnnotation( LongString.class ); } @Override protected void handleFocusReceivedEvent() { this.textField.setFocus(); } 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 ValueProperty ) { return new TextFieldPropertyEditorPresentation( part, parent, composite ); } return null; } } private static final class ContentProposalProvider implements IContentProposalProvider { private ContentProposalService contentProposalService; private ContentProposalService.Session session = null; private SapphirePart sapphirePart; public ContentProposalProvider( ContentProposalService contentProposalService, SapphirePart sapphirePart) { this.contentProposalService = contentProposalService; this.sapphirePart = sapphirePart; } public IContentProposal[] getProposals(String contents, int position) { if (this.session == null) { this.session = this.contentProposalService.session(); } final String oldFilter = this.session.filter(); final int oldFilterLength = oldFilter.length(); final String newFilter = contents.substring( 0, position ); if( position < oldFilterLength || ! oldFilter.equals( newFilter ) ) { this.session = this.contentProposalService.session(); if( position > 0 ) { this.session.advance( newFilter ); } } else if( position > oldFilterLength ) { this.session.advance( newFilter.substring( oldFilterLength ) ); } List<ContentProposal> filterProposals = this.session.proposals(); IContentProposal[] arrContentProposals = makeProposalArray(filterProposals); return arrContentProposals; } private IContentProposal[] makeProposalArray( List<ContentProposal> proposals) { if (proposals != null) { IContentProposal[] arrContentProposals = new IContentProposal[proposals .size()]; for (int i = 0; i < proposals.size(); i++) { ContentProposal contentProposalInfo = proposals.get(i); ImageContentProposal contentProposal = new ImageContentProposal( contentProposalInfo.content(), contentProposalInfo.label(), contentProposalInfo.description(), contentProposalInfo.content().length(), this.sapphirePart.getSwtResourceCache().image( contentProposalInfo.image())); arrContentProposals[i] = contentProposal; } return arrContentProposals; } else { return new IContentProposal[0]; } } } private static final class ContentProposalLabelProvider extends LabelProvider { @Override public Image getImage(Object element) { return ((ImageContentProposal) element).getImage(); } @Override public String getText(Object element) { return ((ImageContentProposal) element).getLabel(); } } private static final class ImageContentProposal extends org.eclipse.jface.fieldassist.ContentProposal { private Image image; public ImageContentProposal(String content, String label, String description, int cursorPosition, Image image) { super(content, label, description, cursorPosition); this.image = image; } public Image getImage() { return this.image; } } }