/*!
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.openformula.ui;
import org.pentaho.openformula.ui.model2.FunctionInformation;
import org.pentaho.openformula.ui.util.FunctionParameterEditHelper;
import org.pentaho.openformula.ui.util.InlineEditTextArea;
import org.pentaho.openformula.ui.util.SelectFieldAction;
import org.pentaho.reporting.libraries.base.util.StringUtils;
import org.pentaho.reporting.libraries.designtime.swing.HorizontalLayout;
import org.pentaho.reporting.libraries.designtime.swing.ToolbarButton;
import org.pentaho.reporting.libraries.formula.DefaultFormulaContext;
import org.pentaho.reporting.libraries.formula.Formula;
import org.pentaho.reporting.libraries.formula.FormulaContext;
import org.pentaho.reporting.libraries.formula.LibFormulaErrorValue;
import org.pentaho.reporting.libraries.formula.function.FunctionDescription;
import org.pentaho.reporting.libraries.formula.lvalues.TypeValuePair;
import org.pentaho.reporting.libraries.formula.parser.ParseException;
import org.pentaho.reporting.libraries.formula.typing.Type;
import org.pentaho.reporting.libraries.formula.typing.TypeUtil;
import org.pentaho.reporting.libraries.formula.util.FormulaUtil;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
public class FormulaEditorPanel extends JComponent implements FieldDefinitionSource {
private class CaretHandler implements CaretListener {
/**
* Called when the caret position is updated.
*
* @param e the caret event
*/
public void caretUpdate( final CaretEvent e ) {
if ( ignoreTextEvents ) {
return;
}
editorModel.setCaretPosition( functionTextArea.getCaretPosition() );
refreshInformationPanel();
revalidateParameters( true );
}
}
/**
* A event handler that keeps the InformationPanel up to date.
*/
private class FunctionDescriptionUpdateHandler implements PropertyChangeListener, ActionListener {
private FunctionDescriptionUpdateHandler() {
}
public void propertyChange( final PropertyChangeEvent evt ) {
refreshInformationPanel();
}
/**
* Invoked when an action occurs.
*
* @noinspection MagicCharacter
*/
public void actionPerformed( final ActionEvent e ) {
final FunctionDescription selectedFunction = functionSelectorPanel.getSelectedValue();
final StringBuilder b = new StringBuilder( 100 );
b.append( selectedFunction.getCanonicalName() );
b.append( '(' );
final int count;
if ( selectedFunction.isInfiniteParameterCount() ) {
count = Math.min( 1, selectedFunction.getParameterCount() );
} else {
count = selectedFunction.getParameterCount();
}
for ( int i = 0; i < count; i++ ) {
if ( i > 0 ) {
b.append( ";" );
}
final Type type = selectedFunction.getParameterType( i );
b.append( TypeUtil.getParameterType( type, getLocale() ) );
}
b.append( ')' );
try {
final Document document = functionTextArea.getDocument();
final int selectionStart = functionTextArea.getSelectionStart();
document.remove( selectionStart, functionTextArea.getSelectionEnd() - selectionStart );
document.insertString( functionTextArea.getCaretPosition(), b.toString(), null );
} catch ( BadLocationException e1 ) {
e1.printStackTrace();
}
}
}
private class DocumentSyncHandler implements PropertyChangeListener {
private DocumentSyncHandler() {
}
public void propertyChange( final PropertyChangeEvent evt ) {
if ( "text".equals( evt.getPropertyName() ) == false ) {
return;
}
if ( ignoreTextEvents ) {
return;
}
run();
}
public void run() {
editorModel.setFormulaText( functionTextArea.getText() );
editorModel.setCaretPosition( functionTextArea.getCaretPosition() );
ignoreTextEvents = false;
revalidateParameters( false );
revalidateFormulaSyntax();
}
}
public class ParameterUpdateHandler implements ParameterUpdateListener {
private ParameterUpdateHandler() {
}
public boolean isEmbeddedFunction( final String parameterText ) {
// Determine if the parameter is a function (i.e. has '(' and ')'). If so,
// then figure if the
if ( parameterText != null ) {
if ( parameterText.contains( "(" ) && parameterText.contains( ")" ) ) {
return true;
}
}
return false;
}
/**
* This method gets called after each parameter text has been entered in the parameter field. If user is manually
* entering text in formula text-area, then this method is called for each character entered. If user is entering a
* formula, the parameter field will not change to the corresponding embedded formula unless user puts their cursor
* on the formula.
*
* @param event
*/
public synchronized void parameterUpdated( final ParameterUpdateEvent event ) {
if ( ignoreTextEvents == true ) {
return;
}
final FunctionInformation fn = editorModel.getCurrentFunction();
if ( fn == null ) {
return;
}
final FunctionParameterEditHelper.EditResult formulaText =
FunctionParameterEditHelper.buildFormulaText( event, fn, editorModel.getFormulaText() );
ignoreTextEvents = true;
// The formula in the formula text-area represents the correct and updated formula text.
// Rebuild the element nodes based on this new representation.
editorModel.setFormulaText( formulaText.text );
// Update for formula text-area
functionTextArea.setText( formulaText.text );
functionTextArea.setCaretPosition( formulaText.caretPositionAfterEdit );
editorModel.setCaretPosition( functionTextArea.getCaretPosition() );
ignoreTextEvents = false;
revalidateParameters( false );
revalidateFormulaSyntax();
}
}
private class FieldSelectorListener implements PropertyChangeListener {
private FieldSelectorListener() {
}
/**
* This method gets called when a bound property is changed.
*
* @param evt A PropertyChangeEvent object describing the event source and the property that has changed.
*/
public void propertyChange( final PropertyChangeEvent evt ) {
final FieldDefinition value = (FieldDefinition) evt.getNewValue();
final String text = FormulaUtil.quoteReference( value.getName() );
insertText( text );
}
}
private class InsertOperatorAction extends AbstractAction {
private String symbol;
private static final int IMAGE_SIZE = 16;
private InsertOperatorAction( final String symbol,
final String description ) {
this.symbol = symbol;
putValue( Action.SMALL_ICON, createImage( symbol ) );
putValue( Action.SHORT_DESCRIPTION, description );
}
/**
* Invoked when an action occurs.
*/
public void actionPerformed( final ActionEvent e ) {
insertText( symbol );
}
private ImageIcon createImage( final String symbol ) {
final BufferedImage bi = new BufferedImage( IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_ARGB );
final Graphics graphics = bi.getGraphics();
final Rectangle2D stringBounds = graphics.getFontMetrics().getStringBounds( symbol, graphics );
final int xspace = (int) Math.max( IMAGE_SIZE - stringBounds.getWidth(), 0 );
final int yspace = (int) Math.max( IMAGE_SIZE - stringBounds.getHeight(), 0 );
graphics.setColor( Color.BLACK );
final double y2 = stringBounds.getY();
final int y1 = (int) ( ( yspace / 2 ) - y2 );
graphics.drawString( symbol, xspace / 2, y1 );
graphics.dispose();
return new ImageIcon( bi );
}
}
private boolean ignoreTextEvents;
private FunctionListPanel functionSelectorPanel;
private MultiplexFunctionParameterEditor functionParameterEditor;
private FunctionInformationPanel functionInformationPanel;
private FormulaContext formulaContext;
private InlineEditTextArea functionTextArea;
private JLabel errorTextHolder;
private JLabel errorIconHolder;
private FieldDefinition[] fields;
private FormulaEditorModel editorModel;
private ImageIcon errorIcon;
private SelectFieldAction selectFieldsAction;
private JToolBar operatorPanel;
private DocumentSyncHandler docSyncHandler;
private ParameterUpdateHandler parameterUpdateHandler;
public FormulaEditorPanel() {
init();
}
public FormulaEditorModel getEditorModel() {
return editorModel;
}
public DocumentSyncHandler getDocSyncHandler() {
return docSyncHandler;
}
public void setDocSyncHandler( final DocumentSyncHandler docSyncHandler ) {
this.docSyncHandler = docSyncHandler;
}
protected MultiplexFunctionParameterEditor getFunctionParameterEditor() {
return functionParameterEditor;
}
protected void insertText( final String text ) {
final int start = functionTextArea.getCaretPosition();
final String formulaTextOriginal = editorModel.getFormulaText();
final StringBuilder formulaText = new StringBuilder( formulaTextOriginal );
// Ensure that only one equal sign in first cursor position exists.
int textLength = text.length();
if ( "=".equals( formulaTextOriginal ) ) {
if ( text.startsWith( "=" ) ) {
formulaText.append( text.substring( 1 ) );
textLength--;
} else {
formulaText.append( text );
}
} else {
String formulaFrag = text;
if ( ( formulaTextOriginal.length() == 0 ) && ( start == 0 ) ) {
formulaFrag = "=" + text;
}
formulaText.insert( start, formulaFrag );
}
ignoreTextEvents = true;
editorModel.setFormulaText( formulaText.toString() );
functionTextArea.setText( formulaText.toString() );
ignoreTextEvents = false;
functionTextArea.setCaretPosition( textLength + start );
functionTextArea.requestFocus();
revalidateParameters( false );
revalidateFormulaSyntax();
}
public JToolBar getOperatorPanel() {
return operatorPanel;
}
public void setEditor( final String function, final FunctionParameterEditor editor ) {
functionParameterEditor.setEditor( function, editor );
}
public FunctionParameterEditor getEditor( final String function ) {
return functionParameterEditor.getEditor( function );
}
public JTextArea getFunctionTextArea() {
return functionTextArea;
}
protected void init() {
editorModel = new FormulaEditorModel();
functionInformationPanel = new FunctionInformationPanel();
functionParameterEditor = new MultiplexFunctionParameterEditor();
parameterUpdateHandler = new ParameterUpdateHandler();
functionParameterEditor.addParameterUpdateListener( parameterUpdateHandler );
functionTextArea = new InlineEditTextArea();
this.setDocSyncHandler( new DocumentSyncHandler() );
functionTextArea.addPropertyChangeListener( "text", getDocSyncHandler() );
functionTextArea.setRows( 6 );
functionTextArea.addCaretListener( new CaretHandler() );
functionTextArea.setFont
( new Font( Font.MONOSPACED, functionTextArea.getFont().getStyle(), functionTextArea.getFont().getSize() ) );
formulaContext = new DefaultFormulaContext();
functionSelectorPanel = new FunctionListPanel();
functionSelectorPanel
.addPropertyChangeListener( "selectedValue", new FunctionDescriptionUpdateHandler() ); // NON-NLS
functionSelectorPanel.addActionListener( new FunctionDescriptionUpdateHandler() );
functionSelectorPanel.setFormulaContext( this.formulaContext );
errorIcon = new ImageIcon( getClass().getResource( "/org/pentaho/openformula/ui/images/error.gif" ) ); // NON-NLS
errorIconHolder = new JLabel();
errorTextHolder = new JLabel();
errorTextHolder.setName( "errorTextHolder" );
selectFieldsAction = new SelectFieldAction( this, new FieldSelectorListener(), this );
final JSplitPane functionPanel = new JSplitPane( JSplitPane.VERTICAL_SPLIT );
functionPanel.setTopComponent( functionParameterEditor.getEditorComponent() );
functionPanel.setBottomComponent( buildFormulaTextPanel() );
functionPanel.setBorder( new EmptyBorder( 0, 0, 0, 0 ) );
setLayout( new BorderLayout() );
setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) );
add( functionSelectorPanel, BorderLayout.WEST );
add( functionInformationPanel, BorderLayout.SOUTH );
add( functionPanel, BorderLayout.CENTER );
}
private JComponent buildFormulaTextPanel() {
operatorPanel = createOperatorPanel();
final JPanel textPanel = new JPanel( new BorderLayout() );
textPanel.setLayout( new GridBagLayout() );
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = 3;
gbc.fill = GridBagConstraints.BOTH;
gbc.insets = new Insets( 5, 0, 5, 0 );
textPanel.add( operatorPanel, gbc );
gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 1;
textPanel.add( new JLabel( Messages.getInstance().getString( "FormulaEditorDialog.Formula" ) ), gbc );
gbc = new GridBagConstraints();
gbc.gridx = 1;
gbc.gridy = 1;
textPanel.add( errorIconHolder, gbc );
gbc = new GridBagConstraints();
gbc.gridx = 2;
gbc.gridy = 1;
gbc.fill = GridBagConstraints.BOTH;
textPanel.add( errorTextHolder, gbc );
gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 2;
gbc.gridwidth = 3;
gbc.weightx = 1;
gbc.weighty = 1;
gbc.fill = GridBagConstraints.BOTH;
textPanel.add( new JScrollPane( functionTextArea ), gbc );
return textPanel;
}
protected JToolBar createOperatorPanel() {
final JToolBar operatorButtonPanel = new JToolBar();
operatorButtonPanel.setFloatable( false );
operatorButtonPanel.setOpaque( false );
operatorButtonPanel.setLayout( new HorizontalLayout( 2 ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( "+", Messages.getInstance().getString( "FormulaEditorDialog.Operator.Add" ) ) ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( "-",
Messages.getInstance().getString( "FormulaEditorDialog.Operator.Subtract" ) ) ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( "*",
Messages.getInstance().getString( "FormulaEditorDialog.Operator.Multiply" ) ) ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( "/", Messages.getInstance().getString( "FormulaEditorDialog.Operator.Divide" ) ) ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( "^", Messages.getInstance().getString( "FormulaEditorDialog.Operator.Power" ) ) ) );
operatorButtonPanel.add( Box.createRigidArea( new Dimension( 10, 1 ) ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( "=", Messages.getInstance().getString( "FormulaEditorDialog.Operator.Equal" ) ) ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( "<>",
Messages.getInstance().getString( "FormulaEditorDialog.Operator.NotEqual" ) ) ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( "<", Messages.getInstance().getString( "FormulaEditorDialog.Operator.Lesser" ) ) ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( ">", Messages.getInstance().getString( "FormulaEditorDialog.Operator.Greater" ) ) ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( "<=",
Messages.getInstance().getString( "FormulaEditorDialog.Operator.LesserEqual" ) ) ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( ">=",
Messages.getInstance().getString( "FormulaEditorDialog.Operator.GreaterEqual" ) ) ) );
operatorButtonPanel.add( Box.createRigidArea( new Dimension( 10, 1 ) ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( "%",
Messages.getInstance().getString( "FormulaEditorDialog.Operator.Percentage" ) ) ) );
operatorButtonPanel.add( new ToolbarButton
( new InsertOperatorAction( "&",
Messages.getInstance().getString( "FormulaEditorDialog.Operator.Concatenation" ) ) ) );
operatorButtonPanel.add( Box.createRigidArea( new Dimension( 10, 1 ) ) );
operatorButtonPanel.add( new ToolbarButton( selectFieldsAction ) );
return operatorButtonPanel;
}
public ParameterUpdateHandler getParameterUpdateHandler() {
return parameterUpdateHandler;
}
public String getFormulaText() {
return functionTextArea.getText();
}
public void setFormulaText( String formulaText ) {
if ( ( formulaText == null ) || ( formulaText.length() == 0 ) ) {
formulaText = "=";
} else if ( formulaText.startsWith( "=" ) == false ) {
formulaText = "=" + formulaText;
}
this.functionTextArea.setText( formulaText );
this.functionTextArea.setCaretPosition( formulaText.length() );
// Update model
editorModel.setFormulaText( formulaText );
editorModel.setCaretPosition( functionTextArea.getCaretPosition() );
// Revalidate parameters and force refresh of parameter fields
revalidateParameters( true );
}
public void setFields( final FieldDefinition[] fields ) {
if ( fields == null ) {
throw new NullPointerException();
}
this.fields = fields.clone();
this.functionParameterEditor.setFields( fields );
}
public FieldDefinition[] getFields() {
return fields.clone();
}
/**
* Re-validate the parameters of the selected formula.
*
* @param switchParameterEditor - if true, then the parameter editor will adjust to correspond to formula in the
* formula text-area. This prevents parameter editor from changing while user is
* entering an embedded formula.
* @noinspection MagicCharacter
*/
protected void revalidateParameters( final boolean switchParameterEditor ) {
editorModel.revalidateStructure();
if ( formulaContext == null ) {
functionParameterEditor.clearSelectedFunction();
return;
}
final FunctionInformation fnInfo = editorModel.getCurrentFunction();
if ( fnInfo == null ) {
functionParameterEditor.clearSelectedFunction();
return;
}
final FunctionDescription fnDesc = formulaContext.getFunctionRegistry().getMetaData( fnInfo.getCanonicalName() );
if ( fnDesc == null ) {
functionParameterEditor.clearSelectedFunction();
return;
}
functionInformationPanel.setSelectedFunction( fnDesc );
try {
ignoreTextEvents = true;
functionParameterEditor.setSelectedFunction( new FunctionParameterContext
( fnDesc, fnInfo, switchParameterEditor, editorModel ) );
} finally {
ignoreTextEvents = false;
}
}
private void refreshInformationPanel() {
final FunctionInformation currentFunction = editorModel.getCurrentFunction();
final FunctionDescription description;
if ( currentFunction != null ) {
description = formulaContext.getFunctionRegistry().getMetaData( currentFunction.getCanonicalName() );
} else {
description = functionSelectorPanel.getSelectedValue();
}
functionInformationPanel.setSelectedFunction( description );
}
protected void revalidateFormulaSyntax() {
try {
final String rawFormula = editorModel.getFormulaText();
if ( StringUtils.isEmpty( rawFormula ) ) {
errorTextHolder.setText( "" );
errorTextHolder.setToolTipText( null );
errorIconHolder.setIcon( null );
return;
}
final String formulaText = FormulaUtil.extractFormula( rawFormula );
if ( StringUtils.isEmpty( formulaText ) ) {
errorTextHolder.setText( Messages.getInstance().getString( "FormulaEditorDialog.ShortErrorNoFormulaContext" ) );
errorTextHolder
.setToolTipText( Messages.getInstance().getString( "FormulaEditorDialog.ErrorNoFormulaContext" ) );
return;
}
final Formula formula = new Formula( formulaText );
formula.initialize( formulaContext );
final TypeValuePair pair = formula.evaluateTyped();
if ( pair.getValue() instanceof LibFormulaErrorValue ) {
errorTextHolder.setText( Messages.getInstance().getString( "FormulaEditorDialog.ShortEvaluationError" ) );
errorTextHolder.setToolTipText( Messages.getInstance().getString( "FormulaEditorDialog.EvaluationError" ) );
} else {
errorTextHolder.setToolTipText( null );
errorTextHolder.setText( Messages.getInstance()
.getString( "FormulaEditorDialog.EvaluationResult", String.valueOf( pair.getValue() ) ) );
}
errorIconHolder.setIcon( null );
} catch ( ParseException pe ) {
errorIconHolder.setIcon( errorIcon );
if ( pe.currentToken == null ) {
errorTextHolder.setText( Messages.getInstance().getString( "FormulaEditorDialog.ShortParseError" ) );
errorTextHolder.setToolTipText(
Messages.getInstance().getString( "FormulaEditorDialog.GenericParseError", pe.getLocalizedMessage() ) );
} else {
final String token = pe.currentToken.toString();
final int line = pe.currentToken.beginLine;
final int column = pe.currentToken.beginColumn;
errorTextHolder.setText( Messages.getInstance().getString( "FormulaEditorDialog.ShortParseError" ) );
errorTextHolder.setToolTipText( Messages.getInstance().getString( "FormulaEditorDialog.ParseError",
new Object[] { token, line, column } ) );
}
} catch ( Exception e ) {
errorIconHolder.setIcon( errorIcon );
errorTextHolder.setText( Messages.getInstance().getString( "FormulaEditorDialog.ShortParseError" ) );
errorTextHolder.setToolTipText(
Messages.getInstance().getString( "FormulaEditorDialog.GenericParseError", e.getLocalizedMessage() ) );
}
}
}