/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2016 by Pentaho : http://www.pentaho.com
*
*******************************************************************************
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/
/**
*
*/
package org.pentaho.libformula.ui.editor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.util.Utils;
import org.pentaho.di.i18n.BaseMessages;
import org.pentaho.di.ui.core.gui.GUIResource;
import org.pentaho.libformula.editor.FormulaEvaluator;
import org.pentaho.libformula.editor.FormulaMessage;
import org.pentaho.libformula.editor.function.FunctionDescription;
import org.pentaho.libformula.editor.function.FunctionLib;
import org.pentaho.libformula.editor.util.CompletionProposal;
import org.pentaho.libformula.editor.util.PositionAndLength;
import org.pentaho.reporting.libraries.formula.lvalues.ParsePosition;
/**
* @author matt
*
*/
public class LibFormulaEditor extends Dialog implements KeyListener {
public static final String FUNCTIONS_FILE = "functions.xml";
private Shell shell;
private Tree tree;
// private TreeEditor treeEditor;
private SashForm sashForm;
private StyledText expressionEditor;
private String formula;
private Browser message;
private Button ok, cancel;
private String[] inputFields;
private Color blue;
private Color red;
private Color green;
private Color white;
private Color gray;
private Color black;
Menu helperMenu;
private FunctionLib functionLib;
private String[] functions;
private String[] categories;
private SashForm rightSash;
private FormulaEvaluator evaluator;
public LibFormulaEditor( Shell parent, int style, String formula, String[] inputFields ) throws Exception {
super( parent, style );
this.formula = formula;
this.inputFields = inputFields;
// Run it in a new shell:
//
shell = new Shell( parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX | SWT.MIN );
// The layout...
//
FormLayout formLayout = new FormLayout();
formLayout.marginWidth = 5;
formLayout.marginHeight = 5;
shell.setLayout( formLayout );
// At the bottom we have a few buttons...
//
Composite buttonsComposite = new Composite( shell, SWT.NONE );
FillLayout bcLayout = new FillLayout();
bcLayout.spacing = 5;
buttonsComposite.setLayout( bcLayout );
ok = new Button( buttonsComposite, SWT.PUSH );
ok.setText( " OK " ); // TODO i18n
cancel = new Button( buttonsComposite, SWT.PUSH );
cancel.setText( " Cancel " ); // TODO i18n
ok.addSelectionListener( new SelectionAdapter() {
public void widgetSelected( SelectionEvent e ) {
ok();
}
} );
cancel.addSelectionListener( new SelectionAdapter() {
public void widgetSelected( SelectionEvent e ) {
cancel();
}
} );
// A tree on the left, an editor on the right: put them in a sash form...
// Right below the editor we display the error messages...
//
sashForm = new SashForm( shell, SWT.HORIZONTAL );
sashForm.setLayout( new FillLayout() );
FormData fdSashForm = new FormData();
fdSashForm.left = new FormAttachment( 0, 0 );
fdSashForm.right = new FormAttachment( 100, 0 );
fdSashForm.top = new FormAttachment( 0, 10 );
fdSashForm.bottom = new FormAttachment( buttonsComposite, -10 );
sashForm.setLayoutData( fdSashForm );
FormData fdBC = new FormData();
fdBC.left = new FormAttachment( sashForm, 0, SWT.CENTER );
fdBC.bottom = new FormAttachment( 100, 0 );
buttonsComposite.setLayoutData( fdBC );
// Read the function descriptions...
//
readFunctions();
evaluator = new FormulaEvaluator( functions, inputFields );
// A tree on the left:
//
tree = new Tree( sashForm, SWT.SINGLE );
for ( int i = 0; i < categories.length; i++ ) {
String category = categories[i];
String i18nCategory = category;
// Look up the category in i18n if needed.
if ( category.startsWith( "%" ) ) {
i18nCategory = BaseMessages.getString( FunctionLib.class, category.substring( 1 ) ); // skip the %
}
TreeItem item = new TreeItem( tree, SWT.NONE );
item.setText( i18nCategory );
String[] fnames = functionLib.getFunctionsForACategory( category );
for ( String fname : fnames ) {
TreeItem fitem = new TreeItem( item, SWT.NONE );
fitem.setText( fname );
}
}
/**
* If someone clicks on a function, we display the description of the function in the message box...
*/
tree.addSelectionListener( new SelectionAdapter() {
public void widgetSelected( SelectionEvent event ) {
if ( tree.getSelectionCount() == 1 ) {
TreeItem item = tree.getSelection()[0];
if ( item.getParentItem() != null ) { // has a category above it
String functionName = item.getText();
FunctionDescription functionDescription = functionLib.getFunctionDescription( functionName );
if ( functionDescription != null ) {
String report = functionDescription.getHtmlReport();
message.setText( report );
}
}
}
}
} );
rightSash = new SashForm( sashForm, SWT.VERTICAL );
// An expression editor on the right
//
expressionEditor = new StyledText( rightSash, SWT.NONE );
expressionEditor.setText( this.formula );
expressionEditor.addModifyListener( new ModifyListener() {
public void modifyText( ModifyEvent event ) {
setStyles();
}
} );
expressionEditor.addKeyListener( this );
// Some information concerning the validity of the formula expression
//
message = new Browser( rightSash, SWT.MULTI | SWT.READ_ONLY | SWT.V_SCROLL | SWT.H_SCROLL );
FormData fdMessage = new FormData();
fdMessage.left = new FormAttachment( 0, 0 );
fdMessage.right = new FormAttachment( 100, 0 );
fdMessage.top = new FormAttachment( 0, 0 );
fdMessage.bottom = new FormAttachment( 0, 100 );
message.setLayoutData( fdMessage );
rightSash.setWeights( new int[] { 10, 80, } );
sashForm.setWeights( new int[] { 15, 85, } );
red = new Color( shell.getDisplay(), 255, 0, 0 );
green = new Color( shell.getDisplay(), 0, 220, 0 );
blue = new Color( shell.getDisplay(), 0, 0, 255 );
white = new Color( shell.getDisplay(), 255, 255, 255 );
gray = new Color( shell.getDisplay(), 150, 150, 150 );
black = new Color( shell.getDisplay(), 0, 0, 0 );
setStyles();
shell.addDisposeListener( new DisposeListener() {
public void widgetDisposed( DisposeEvent arg0 ) {
red.dispose();
green.dispose();
blue.dispose();
white.dispose();
gray.dispose();
black.dispose();
}
} );
}
public String open() {
shell.layout();
shell.open();
// Detect X or ALT-F4 or something that kills this window...
shell.addShellListener( new ShellAdapter() {
public void shellClosed( ShellEvent e ) {
cancel();
}
} );
while ( !shell.isDisposed() ) {
if ( !shell.getDisplay().readAndDispatch() ) {
shell.getDisplay().sleep();
}
}
return formula;
}
public void ok() {
formula = expressionEditor.getText();
shell.dispose();
}
public void cancel() {
formula = null;
shell.dispose();
}
public void readFunctions() throws Exception {
// URL url =
// this.getClass().getResource("/"+FunctionLib.class.getPackage().getName().replace(".","/")+"/"+FUNCTIONS_FILE);
functionLib = new FunctionLib( FUNCTIONS_FILE );
functions = functionLib.getFunctionNames();
categories = functionLib.getFunctionCategories();
}
public void setStyles() {
String expression = expressionEditor.getText();
int expressionLength = expression.length();
Map<String, FormulaMessage> messages = evaluator.evaluateFormula( expression );
// We need to provide an array of styles for this event.
//
Vector<StyleRange> styles = new Vector<StyleRange>();
StringBuilder report = new StringBuilder();
for ( FormulaMessage message : messages.values() ) {
ParsePosition position = message.getPosition();
PositionAndLength positionAndLength = PositionAndLength.calculatePositionAndLength( expression, position );
int pos = positionAndLength.getPosition();
int length = positionAndLength.getLength();
if ( pos < expressionLength ) {
switch ( message.getType() ) {
case FormulaMessage.TYPE_ERROR:
report.append( message.toString() ).append( Const.CR );
StyleRange styleRangeRed = new StyleRange( pos, length, red, null, SWT.BOLD );
styleRangeRed.underline = true;
styles.add( styleRangeRed );
break;
case FormulaMessage.TYPE_FUNCTION:
styles.add( new StyleRange( pos, length, black, null, SWT.BOLD ) );
break;
case FormulaMessage.TYPE_FIELD:
// styles.add(new StyleRange(pos, length, green, null, SWT.BOLD )); // TODO : Not working for some reason.
break;
case FormulaMessage.TYPE_STATIC_NUMBER:
case FormulaMessage.TYPE_STATIC_STRING:
case FormulaMessage.TYPE_STATIC_DATE:
case FormulaMessage.TYPE_STATIC_LOGICAL:
styles.add( new StyleRange( pos, length, blue, gray, SWT.BOLD | SWT.ITALIC ) );
break;
default:
break;
}
}
}
message.setText( report.toString() );
// Now set the styled ranges...
//
// Sort the styles first...
//
Collections.sort( styles, new Comparator<StyleRange>() {
public int compare( StyleRange o1, StyleRange o2 ) {
return o1.start - o2.start;
}
} );
StyleRange[] styleRanges = new StyleRange[styles.size()];
styles.copyInto( styleRanges );
// expressionEditor.getStyledText().replaceStyleRanges(0, expression.length(), new StyleRange[] { styles.get(0), });
expressionEditor.setStyleRanges( styleRanges );
}
public static void main( String[] args ) throws Exception {
Display display = new Display();
String[] inputFields = { "firstname", "name", };
LibFormulaEditor lbe =
new LibFormulaEditor(
new Shell( display, SWT.NONE ), SWT.NONE, "MID(UPPER([name] & \" \" & [firstname]);5;10)", inputFields );
lbe.open();
}
public void keyPressed( KeyEvent e ) {
boolean ctrl = ( ( e.stateMask & SWT.CONTROL ) != 0 );
// boolean alt = ((e.stateMask & SWT.ALT) != 0);
List<CompletionProposal> proposals = new ArrayList<CompletionProposal>();
// CTRL-SPACE?
//
if ( ctrl && e.character == ' ' ) {
// Gab the content before the cursor position...
//
StringBuilder beforeBuffer = new StringBuilder();
String editor = expressionEditor.getText();
int pos = expressionEditor.getCaretOffset() - 1;
while ( pos >= 0 && pos < editor.length() ) {
char c = editor.charAt( pos );
if ( Character.isWhitespace( c ) ) {
break;
}
if ( Character.isLetterOrDigit( c ) || c == '[' ) {
beforeBuffer.insert( 0, c );
pos--;
} else {
break;
}
}
String before = beforeBuffer.toString();
System.out.println( "BEFORE = " + before );
// if we just have [ we display only the field names...
//
if ( before.equals( "[" ) ) {
for ( String fieldName : inputFields ) {
proposals.add( new CompletionProposal( "[" + fieldName + "] (input field)", fieldName + "]", fieldName
.length() + 1 ) );
}
} else if ( Utils.isEmpty( before ) ) {
for ( String fieldName : inputFields ) {
proposals.add( new CompletionProposal(
"[" + fieldName + "] (input field)", "[" + fieldName + "]", fieldName.length() + 2 ) );
}
} else {
// Only add those where "before" matches the start of the keyword or function
//
for ( String fieldName : inputFields ) {
String key = "[" + fieldName;
if ( key.startsWith( before ) && !key.equalsIgnoreCase( before ) ) {
proposals.add( new CompletionProposal( "[" + fieldName + "] (keyword)", fieldName.substring( before
.length() )
+ "]", fieldName.length() - before.length() + 1 ) );
}
}
for ( String function : functions ) {
if ( function.startsWith( before ) && !function.equalsIgnoreCase( before ) ) {
proposals.add( new CompletionProposal( function + "() (Function)", function
.substring( before.length() )
+ "()", function.length() - before.length() + 1 ) );
}
}
}
if ( helperMenu == null ) {
helperMenu = new Menu( shell, SWT.POP_UP );
} else {
for ( MenuItem item : helperMenu.getItems() ) {
item.dispose();
}
}
// final int offset = expressionEditor.getCaretOffset();
final int offset = expressionEditor.getCaretOffset();
Point p = expressionEditor.getLocationAtOffset( offset );
int h = expressionEditor.getLineHeight( offset );
Point l = GUIResource.calculateControlPosition( expressionEditor );
MenuItem first = null;
if ( proposals.size() == 1 ) {
MenuItem item = new MenuItem( helperMenu, SWT.NONE );
if ( first == null ) {
first = item;
}
final CompletionProposal proposal = proposals.get( 0 );
item.setText( proposal.getMenuText() );
item.addSelectionListener( new SelectionAdapter() {
public void widgetSelected( SelectionEvent se ) {
expressionEditor.insert( proposal.getCompletionString() );
expressionEditor.setSelection( offset + proposal.getOffset() );
}
} );
helperMenu.setLocation( l.x + p.x, l.y + p.y + h );
helperMenu.setDefaultItem( first );
helperMenu.setVisible( true );
} else if ( proposals.size() > 0 ) {
int nr = 0;
for ( final CompletionProposal proposal : proposals ) {
MenuItem item = new MenuItem( helperMenu, SWT.NONE );
if ( first == null ) {
first = item;
}
item.setText( proposal.getMenuText() );
item.addSelectionListener( new SelectionAdapter() {
public void widgetSelected( SelectionEvent se ) {
expressionEditor.insert( proposal.getCompletionString() );
expressionEditor.setSelection( offset + proposal.getOffset() );
}
} );
if ( nr++ > 5 ) {
break;
}
}
helperMenu.setLocation( l.x + p.x, l.y + p.y + h );
helperMenu.setDefaultItem( first );
helperMenu.setVisible( true );
}
}
}
public void keyReleased( KeyEvent arg0 ) {
// TODO Auto-generated method stub
}
}