/****************************************************************************** * 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.SwtUtil.runOnDisplayThread; import org.eclipse.core.runtime.Platform; import org.eclipse.jface.resource.JFaceColors; import org.eclipse.sapphire.FilteredListener; import org.eclipse.sapphire.PropertyDefaultEvent; import org.eclipse.sapphire.Serialization; import org.eclipse.sapphire.Value; import org.eclipse.sapphire.ValueProperty; import org.eclipse.sapphire.modeling.annotations.SensitiveData; import org.eclipse.sapphire.ui.Presentation; import org.eclipse.sapphire.ui.forms.JumpActionHandler; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.BusyIndicator; 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.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.TextLayout; import org.eclipse.swt.graphics.TextStyle; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Text; /** * Implements default value overlay and Ctrl+Click navigation in a standard SWT Text widget. * * @author <a href="konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a> */ public final class TextOverlayPainter { private static final Point TEXT_OFFSET; static { final String os = Platform.getOS(); if( os.equals( Platform.OS_WIN32 ) ) { TEXT_OFFSET = new Point( 4, 1 ); } else { // This number has been derived on openSUSE 11.0, but we will use it // for all non-windows systems for now. TEXT_OFFSET = new Point( 2, 2 ); } } private final Display display; private final Text textControl; private final Value<?> property; private final boolean isSensitiveData; private final Serialization serialization; private final JumpActionHandler jumpActionHandler; private final Presentation presentation; private boolean controlKeyActive; private Point textExtent; private boolean hyperlinkActive; private boolean mouseOverText; private TextOverlayPainter( final Text textControl, final Value<?> property, final JumpActionHandler jumpActionHandler, final Presentation presentation ) { final ValueProperty pdef = property.definition(); this.display = textControl.getDisplay(); this.textControl = textControl; this.property = property; this.isSensitiveData = pdef.hasAnnotation( SensitiveData.class ); this.serialization = pdef.getAnnotation( Serialization.class ); this.jumpActionHandler = jumpActionHandler; this.presentation = presentation; this.controlKeyActive = false; this.hyperlinkActive = false; this.mouseOverText = false; final Listener keyListener = new Listener() { public void handleEvent( final Event event ) { handleKeyEvent( event ); } }; this.display.addFilter( SWT.KeyDown, keyListener ); this.display.addFilter( SWT.KeyUp, keyListener ); this.textControl.addDisposeListener ( new DisposeListener() { public void widgetDisposed( final DisposeEvent event ) { TextOverlayPainter.this.display.removeFilter( SWT.KeyDown, keyListener ); TextOverlayPainter.this.display.removeFilter( SWT.KeyUp, keyListener ); } } ); this.textControl.addModifyListener ( new ModifyListener() { public void modifyText( final ModifyEvent event ) { updateTextExtent(); } } ); updateTextExtent(); this.textControl.addListener ( SWT.Paint, new Listener() { public void handleEvent( final Event event ) { handlePaint( event ); } } ); this.textControl.addListener ( SWT.MouseMove, new Listener() { public void handleEvent( final Event event ) { handleMouseMove( event ); } } ); this.textControl.addListener ( SWT.MouseExit, new Listener() { public void handleEvent( final Event event ) { handleMouseExit( event ); } } ); this.textControl.addListener ( SWT.MouseDown, new Listener() { public void handleEvent( final Event event ) { handleMouseDown( event ); } } ); final FilteredListener<PropertyDefaultEvent> propertyListener = new FilteredListener<PropertyDefaultEvent>() { @Override protected void handleTypedEvent( final PropertyDefaultEvent event ) { runOnDisplayThread ( new Runnable() { public void run() { TextOverlayPainter.this.textControl.redraw(); } } ); } }; this.property.attach( propertyListener ); this.textControl.addDisposeListener ( new DisposeListener() { @Override public void widgetDisposed( final DisposeEvent event ) { TextOverlayPainter.this.property.detach( propertyListener ); } } ); } public static void install( final Text textControl, final Value<?> property ) { install( textControl, property, null, null ); } public static void install( final Text textControl, final Value<?> property, final JumpActionHandler jumpActionHandler, final Presentation presentation ) { new TextOverlayPainter( textControl, property, jumpActionHandler, presentation ); } private void handleKeyEvent( final Event event ) { if( event.keyCode == SWT.CONTROL ) { this.controlKeyActive = ( event.type == SWT.KeyDown ); // Only force update when user releases the control key. We want the hyperlink // to show only after user starts moving the mouse after holding down the // control key. if( ! this.controlKeyActive ) { update(); } } } private void handleMouseMove( final Event event ) { if( event.x <= this.textExtent.x && event.y <= this.textExtent.y ) { this.mouseOverText = true; } else { this.mouseOverText = false; } update(); } private void handleMouseExit( final Event event ) { this.mouseOverText = false; update(); } private void handleMouseDown( final Event event ) { if( this.hyperlinkActive ) { this.textControl.setCursor( null ); handleJumpCommand(); } } private boolean isJumpEnabled() { return ( this.jumpActionHandler == null ? false : this.jumpActionHandler.isEnabled() ); } private void handleJumpCommand() { final Runnable op = new Runnable() { public void run() { TextOverlayPainter.this.jumpActionHandler.execute( TextOverlayPainter.this.presentation ); } }; BusyIndicator.showWhile( this.display, op ); } private String overlay() { String def = this.property.disposed() ? null : this.property.getDefaultText(); if( def != null && this.isSensitiveData ) { final StringBuilder buf = new StringBuilder(); for( int i = 0, n = def.length(); i < n; i++ ) { buf.append( "\u25CF" ); } def = buf.toString(); } if( def == null && this.serialization != null ) { def = this.serialization.primary(); } return def; } private void update() { final boolean shouldHyperlinkBeActive = ( this.controlKeyActive && this.mouseOverText && isJumpEnabled() ); if( this.hyperlinkActive != shouldHyperlinkBeActive ) { this.hyperlinkActive = shouldHyperlinkBeActive; this.textControl.setCursor( shouldHyperlinkBeActive ? this.display.getSystemCursor( SWT.CURSOR_HAND ) : null ); this.textControl.redraw(); } } private void updateTextExtent() { final GC gc = new GC( this.textControl ); this.textExtent = gc.textExtent( getTextWithOverlay() ); gc.dispose(); } private void handlePaint( final Event event ) { if( this.textControl.isEnabled() ) { if( this.hyperlinkActive ) { final TextStyle style = new TextStyle( this.textControl.getFont(), null, null ); style.underline = true; style.foreground = JFaceColors.getActiveHyperlinkText( this.display ); style.underlineColor = style.foreground; paintTextOverlay( event.gc, style, getTextWithOverlay() ); } else if( ! this.textControl.isFocusControl() && this.textControl.getText().length() == 0 ) { final String overlay = overlay(); if( overlay != null && overlay.length() > 0 ) { final TextStyle style = new TextStyle( this.textControl.getFont(), null, null ); style.foreground = this.display.getSystemColor( SWT.COLOR_GRAY ); paintTextOverlay( event.gc, style, overlay ); } } } } private void paintTextOverlay( final GC gc, final TextStyle style, final String text ) { final TextLayout layout = new TextLayout( this.display ); layout.setText( text ); layout.setStyle( style, 0, text.length() - 1 ); final Rectangle clientArea = this.textControl.getClientArea(); gc.fillRectangle( clientArea ); layout.setWidth( clientArea.width - TEXT_OFFSET.x * 2 ); layout.draw( gc, TEXT_OFFSET.x, TEXT_OFFSET.y ); } private String getTextWithOverlay() { String text = this.textControl.getText(); if( text.length() == 0 ) { text = overlay(); if( text == null ) { text = ""; } } return text; } }