/******************************************************************************
* 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:
* 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.gdhfill;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gdhindent;
import static org.eclipse.sapphire.ui.forms.swt.GridLayoutUtil.gdwhint;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.sapphire.LocalizableText;
import org.eclipse.sapphire.Text;
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.SapphireActionSystem;
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.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
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.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
/**
* Implements Ctrl+Click navigation in a standard SWT table widget.
*
* @author <a href="konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a>
*/
public final class HyperlinkTable
{
public static abstract class Controller
{
public boolean isHyperlinkEnabled( final TableItem item,
final int column )
{
return true;
}
public abstract void handleHyperlinkEvent( final TableItem item,
final int column );
}
@Text( "Select Jump Destination" )
private static LocalizableText jumpDialogTitle;
@Text( "Where do you want to jump to?" )
private static LocalizableText jumpDialogPrompt;
static
{
LocalizableText.init( HyperlinkTable.class );
}
private static final Point IMAGE_OFFSET_PRIMARY_COLUMN;
private static final Point IMAGE_OFFSET_SECONDARY_COLUMN;
private static final Point TEXT_OFFSET_PRIMARY_COLUMN;
private static final Point TEXT_OFFSET_SECONDARY_COLUMN;
static
{
final String os = Platform.getOS();
final String osName = System.getProperties().getProperty( "os.name" );
if( os.equals( Platform.OS_WIN32 ) )
{
if( osName.equals( "Windows XP" ) )
{
IMAGE_OFFSET_PRIMARY_COLUMN = new Point( 0, 0 );
IMAGE_OFFSET_SECONDARY_COLUMN = new Point( 0, 0 );
TEXT_OFFSET_PRIMARY_COLUMN = new Point( 0, 2 );
TEXT_OFFSET_SECONDARY_COLUMN = new Point( -1, 2 );
}
else
{
IMAGE_OFFSET_PRIMARY_COLUMN = new Point( 0, 1 );
IMAGE_OFFSET_SECONDARY_COLUMN = new Point( 0, 1 );
TEXT_OFFSET_PRIMARY_COLUMN = new Point( 0, 2 );
TEXT_OFFSET_SECONDARY_COLUMN = new Point( 0, 2 );
}
}
else
{
// This number has been derived on openSUSE 11.0, but we will use it
// for all non-windows systems for now.
IMAGE_OFFSET_PRIMARY_COLUMN = new Point( 1, 3 );
IMAGE_OFFSET_SECONDARY_COLUMN = new Point( 1, 3 );
TEXT_OFFSET_PRIMARY_COLUMN = new Point( 1, 3 );
TEXT_OFFSET_SECONDARY_COLUMN = new Point( 1, 3 );
}
}
private boolean controlKeyActive;
private final Table table;
private TableItem mouseOverTableItem;
private int mouseOverColumn;
private Controller controller;
public HyperlinkTable( final Table table,
final SapphireActionGroup actions )
{
this.table = table;
this.controlKeyActive = false;
this.mouseOverTableItem = null;
this.mouseOverColumn = -1;
final Listener keyListener = new Listener()
{
public void handleEvent( final Event event )
{
handleKeyEvent( event );
}
};
final Display display = this.table.getDisplay();
display.addFilter( SWT.KeyDown, keyListener );
display.addFilter( SWT.KeyUp, keyListener );
this.table.addListener
(
SWT.EraseItem,
new Listener()
{
public void handleEvent( final Event event )
{
handleEraseItem( event );
}
}
);
this.table.addListener
(
SWT.PaintItem,
new Listener()
{
public void handleEvent( final Event event )
{
handlePaintItem( event );
}
}
);
this.table.addListener
(
SWT.MouseMove,
new Listener()
{
public void handleEvent( final Event event )
{
handleMouseMove( event );
}
}
);
this.table.addListener
(
SWT.MouseExit,
new Listener()
{
public void handleEvent( final Event event )
{
handleMouseExit( event );
}
}
);
this.table.addListener
(
SWT.MouseDown,
new Listener()
{
public void handleEvent( final Event event )
{
handleMouseDown( event );
}
}
);
final SapphireActionHandler jumpActionHandler = new SapphireActionHandler()
{
@Override
protected Object run( final Presentation context )
{
handleJumpCommand();
return null;
}
};
final SapphireAction jumpAction = actions.getAction( SapphireActionSystem.ACTION_JUMP );
jumpActionHandler.init( jumpAction, null );
jumpAction.addHandler( jumpActionHandler );
this.table.addDisposeListener
(
new DisposeListener()
{
public void widgetDisposed( final DisposeEvent event )
{
display.removeFilter( SWT.KeyDown, keyListener );
display.removeFilter( SWT.KeyUp, keyListener );
jumpAction.removeHandler( jumpActionHandler );
}
}
);
}
public void setController( final Controller controller )
{
this.controller = controller;
}
public Table getTable()
{
return this.table;
}
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 )
{
this.mouseOverTableItem = null;
this.mouseOverColumn = -1;
for( int i = this.table.getTopIndex(), n = this.table.getItemCount(); i < n; i++ )
{
final TableItem item = this.table.getItem( i );
for( int j = 0, m = getColumnCount( this.table ); j < m; j++ )
{
final Rectangle bounds = item.getTextBounds( j );
if( bounds.contains( event.x, event.y ) )
{
final GC gc = new GC( this.table );
final Point textExtent = gc.textExtent( item.getText( j ) );
gc.dispose();
bounds.width = textExtent.x;
bounds.height = textExtent.y;
if( bounds.contains( event.x, event.y ) && this.controller.isHyperlinkEnabled( item, j ) )
{
this.mouseOverTableItem = item;
this.mouseOverColumn = j;
}
break;
}
}
}
update();
}
private void handleMouseExit( final Event event )
{
this.mouseOverTableItem = null;
this.mouseOverColumn = -1;
update();
}
private void handleMouseDown( final Event event )
{
if( this.controlKeyActive && this.mouseOverTableItem != null )
{
final TableItem item = this.mouseOverTableItem;
this.table.setCursor( null );
// Ideally, it would be best to prevent table selection from taking place. Haven't found
// a way to do that yet. At the very least, the following makes sure that Ctrl+Click hyperlink
// action doesn't also have a multi-select behavior.
this.table.setSelection( item );
handleJumpCommand( this.mouseOverTableItem, this.mouseOverColumn );
}
}
private void handleJumpCommand()
{
final TableItem[] items = HyperlinkTable.this.table.getSelection();
if( items.length == 1 )
{
final TableItem item = items[ 0 ];
final List<Integer> columnsWithHyperlinks = new ArrayList<Integer>();
for( int i = 0, n = getColumnCount( HyperlinkTable.this.table ); i < n; i++ )
{
if( this.controller.isHyperlinkEnabled( item, i ) )
{
columnsWithHyperlinks.add( i );
}
}
if( columnsWithHyperlinks.size() == 1 )
{
handleJumpCommand( item, columnsWithHyperlinks.get( 0 ) );
}
else if( ! columnsWithHyperlinks.isEmpty() )
{
final Dialog dialog = new Dialog( this.table.getShell() )
{
private int choice = columnsWithHyperlinks.get( 0 );
@Override
protected Control createDialogArea( final Composite parent )
{
getShell().setText( jumpDialogTitle.text() );
final Composite composite = (Composite) super.createDialogArea( parent );
final Label prompt = new Label( composite, SWT.WRAP );
prompt.setLayoutData( gdwhint( gdhfill(), 300 ) );
prompt.setText( jumpDialogPrompt.text() );
final SelectionListener listener = new SelectionAdapter()
{
public void widgetSelected( final SelectionEvent event )
{
setChoice( (Integer) event.widget.getData() );
}
};
boolean first = true;
for( Integer col : columnsWithHyperlinks )
{
final Button button = new Button( composite, SWT.RADIO | SWT.WRAP );
button.setLayoutData( gdhindent( gd(), 10 ) );
button.setText( item.getText( col ) );
button.setData( col );
if( first )
{
button.setSelection( true );
first = false;
}
button.addSelectionListener( listener );
}
return composite;
}
@Override
protected void okPressed()
{
super.okPressed();
handleJumpCommand( item, this.choice );
}
private void setChoice( final int choice )
{
this.choice = choice;
}
};
dialog.open();
}
}
}
private void handleJumpCommand( final TableItem item,
final int column )
{
final Runnable op = new Runnable()
{
public void run()
{
HyperlinkTable.this.controller.handleHyperlinkEvent( item, column );
}
};
BusyIndicator.showWhile( this.table.getDisplay(), op );
}
private void handleEraseItem( final Event event )
{
final TableItem item = (TableItem) event.item;
if( this.controlKeyActive && this.mouseOverTableItem == item )
{
event.detail &= ~SWT.FOREGROUND;
}
}
private void handlePaintItem( final Event event )
{
final TableItem item = (TableItem) event.item;
if( this.controlKeyActive && this.mouseOverTableItem == item )
{
for( int i = 0, n = getColumnCount( this.table ); i < n; i++ )
{
final Display display = this.table.getDisplay();
final String text = item.getText( i );
final Font font = item.getFont( i );
final TextStyle style = new TextStyle( font, null, null );
if( this.mouseOverColumn == i )
{
final Color hyperlinkColor = JFaceColors.getActiveHyperlinkText( display );
style.underline = true;
style.foreground = hyperlinkColor;
style.underlineColor = hyperlinkColor;
}
final Image image = item.getImage( i );
if( image != null )
{
final Rectangle bounds = item.getBounds( i );
final Point offset = ( i == 0 ? IMAGE_OFFSET_PRIMARY_COLUMN : IMAGE_OFFSET_SECONDARY_COLUMN );
event.gc.drawImage( image, bounds.x + offset.x, bounds.y + offset.y );
}
final TextLayout layout = new TextLayout( display );
layout.setText( text );
layout.setStyle( style, 0, text.length() - 1 );
final Point offset = ( i == 0 ? TEXT_OFFSET_PRIMARY_COLUMN : TEXT_OFFSET_SECONDARY_COLUMN );
final Rectangle clientArea = item.getTextBounds( i );
layout.setWidth( clientArea.width );
layout.draw( event.gc, clientArea.x + offset.x, clientArea.y + offset.y );
}
}
}
private void update()
{
if( this.controlKeyActive && this.mouseOverTableItem != null )
{
this.table.setCursor( this.table.getDisplay().getSystemCursor( SWT.CURSOR_HAND ) );
}
else
{
this.table.setCursor( null );
}
this.table.redraw();
}
private static int getColumnCount( final Table table )
{
final int count = table.getColumnCount();
return ( count == 0 ? 1 : count );
}
}