/*
* 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) 2009 Pentaho Corporation. All rights reserved.
*/
package org.pentaho.openformula.ui;
import org.pentaho.openformula.ui.model2.FormulaElement;
import org.pentaho.openformula.ui.model2.FunctionInformation;
import org.pentaho.openformula.ui.util.InlineEditTextField;
import org.pentaho.openformula.ui.util.SelectFieldAction;
import org.pentaho.openformula.ui.util.TooltipLabel;
import org.pentaho.reporting.libraries.base.util.StringUtils;
import org.pentaho.reporting.libraries.designtime.swing.BorderlessButton;
import org.pentaho.reporting.libraries.formula.function.FunctionDescription;
import org.pentaho.reporting.libraries.formula.util.FormulaUtil;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Locale;
public class DefaultFunctionParameterEditor extends JPanel implements FunctionParameterEditor, FieldDefinitionSource {
private class FieldSelectorUpdateHandler implements PropertyChangeListener {
private int paramIndex;
private FieldSelectorUpdateHandler( final int paramIndex ) {
this.paramIndex = paramIndex;
}
@Override
public void propertyChange( final PropertyChangeEvent evt ) {
final FieldDefinition value = (FieldDefinition) evt.getNewValue();
//noinspection MagicCharacter,StringConcatenation
if ( value != null ) {
final String text = FormulaUtil.quoteReference( value.getName() );
final String parameterValue = getParameterValue( paramIndex );
final TextFieldHolderStruct fieldStruct = getParameterField( paramIndex );
final InlineEditTextField field = fieldStruct.getTextFields();
final StringBuilder b = new StringBuilder( parameterValue );
// remove the selected content, if any
b.delete( field.getSelectionStart(), field.getSelectionEnd() );
// then insert the new content at the cursor position
final int caretPosition = field.getCaretPosition();
b.insert( caretPosition, text );
fieldStruct.setText( b.toString() );
}
}
}
private class FocusListenerHandler extends FocusAdapter {
private InlineEditTextField paramTextField;
private int parameterIndex;
private FocusListenerHandler( final InlineEditTextField paramTextField, final int parameterIndex ) {
this.paramTextField = paramTextField;
this.parameterIndex = parameterIndex;
}
public void focusLost( final FocusEvent e ) {
handleFocusChange();
}
@Override
public void focusGained( final FocusEvent e ) {
handleFocusChange();
}
private void handleFocusChange() {
if ( inSetupUpdate ) {
return;
}
final String s = paramTextField.getText();
fireParameterUpdate( parameterIndex, s );
}
}
private static class TextFieldHolderStruct {
private InlineEditTextField textFields;
private SelectFieldAction selectFieldAction;
private FocusListenerHandler focusHandler;
private Component[] extraComponents;
protected TextFieldHolderStruct( final InlineEditTextField textFields,
final SelectFieldAction selectFieldAction,
final FocusListenerHandler focusHandler,
final Component... extraComponents ) {
this.textFields = textFields;
this.selectFieldAction = selectFieldAction;
this.focusHandler = focusHandler;
this.extraComponents = extraComponents;
}
protected InlineEditTextField getTextFields() {
return textFields;
}
public void setText( final String text ) {
textFields.setText( text );
if ( text != null ) {
textFields.setCaretPosition( text.length() );
}
}
public String getText() {
return textFields.getText();
}
public void dispose() {
selectFieldAction.dispose();
textFields.getParent().remove( textFields );
for ( final Component c : extraComponents ) {
c.getParent().remove( c );
}
}
}
public static final int FIELDS_ADD = 2;
private static final TextFieldHolderStruct[] EMPTY_FIELDS = new TextFieldHolderStruct[ 0 ];
private static final FieldDefinition[] EMPTY_FIELDDEF = new FieldDefinition[ 0 ];
private FunctionDescription selectedFunction;
private JPanel parameterPane;
private FieldDefinition[] fields;
private TextFieldHolderStruct[] textFields;
private boolean inSetupUpdate;
private int parameterUpdatedCount;
/**
* Creates a new <code>JPanel</code> with a double buffer and a flow layout.
*/
public DefaultFunctionParameterEditor() {
parameterPane = new JPanel();
parameterPane.setLayout( new GridBagLayout() );
this.inSetupUpdate = false;
this.parameterUpdatedCount = -1;
this.textFields = EMPTY_FIELDS;
this.fields = EMPTY_FIELDDEF;
final JPanel parameterPaneCarrier = new JPanel();
parameterPaneCarrier.setLayout( new BorderLayout() );
parameterPaneCarrier.add( parameterPane, BorderLayout.NORTH );
final JScrollPane comp = new JScrollPane( parameterPaneCarrier );
comp.setBorder( new EmptyBorder( 0, 0, 0, 0 ) );
comp.setViewportBorder( new EmptyBorder( 0, 0, 0, 0 ) );
setLayout( new CardLayout() );
add( "2", comp );
add( "1", Box.createRigidArea( new Dimension( 650, 250 ) ) );
}
public FunctionDescription getSelectedFunction() {
return selectedFunction;
}
@Override
public void clearSelectedFunction() {
setSelectedFunction( new FunctionParameterContext() );
}
/**
* Determines whether the current context formula is the main one (the first formula following the '='). So
* '=COUNT(1;SUM(1;2;3))', COUNT would be the main formula. If context points to SUM then we return false.
*
* @param context
* @return - true if the context points to the left most outer formula.
*/
public boolean isMainFormula( final FunctionParameterContext context ) {
final FormulaEditorModel editorModel = context.getEditorModel();
if ( ( editorModel == null ) || ( editorModel.getLength() < 1 ) ) {
return true;
}
final FormulaElement mainFormulaElement = editorModel.getFormulaElementAt( 1 );
final FunctionInformation currentFunction = editorModel.getCurrentFunction();
if ( ( mainFormulaElement != null ) && ( currentFunction != null ) && ( currentFunction.getFunctionOffset() == 1 )
&&
( mainFormulaElement.getText().equals( currentFunction.getCanonicalName() ) ) ) {
return true;
} else {
return false;
}
}
/**
* If user is typing in formula text-area, this method updates the appropriate parameter field. Note that the
* parameter fields are not always visible so if they are not visible then return false. Note that when user is
* typing in formula text-area and they are typing an embedded formula, the parameter fields for that embedded formula
* don't get displayed. They get displayed if user points cursor over the formula or arrows over the formula - just
* not when typing.
*
* @param context
* @return
*/
private boolean updateCurrentParameterField( final FunctionParameterContext context ) {
final FunctionDescription selectedFunction = context.getFunction();
final String[] parameterValues = context.getParameterValues();
// Iterate over each parameter field looking to find the field associated with
// the embedded formula. If we find it, build up the formula in parameter field
// to reflect what was typed into the formula text-area
for ( int i = 0; i < textFields.length; i++ ) {
final String parameterValue = textFields[ i ].getText();
if ( ( parameterValue != null ) && ( parameterValue.startsWith( selectedFunction.getCanonicalName() )
== true ) ) {
String updatedFormula = selectedFunction.getCanonicalName() + "(";
for ( int paramIndex = 0; paramIndex < parameterValues.length; paramIndex++ ) {
if ( parameterValues[ paramIndex ] != null ) {
updatedFormula = updatedFormula + parameterValues[ paramIndex ];
updatedFormula += ";";
}
}
// Remove the trailing semicolon
if ( updatedFormula.endsWith( ";" ) ) {
updatedFormula = updatedFormula.substring( 0, updatedFormula.length() - 1 );
}
if ( parameterValue.endsWith( ")" ) ) {
updatedFormula += ")";
}
textFields[ i ].setText( updatedFormula );
return true;
}
}
// We did not find the corresponding parameter field as it is not being displayed
return false;
}
private void updateParameterFields( final String[] parameterValues ) {
if ( parameterValues != null && parameterValues.length <= textFields.length ) {
for ( int i = 0; i < parameterValues.length; i++ ) {
final String string = parameterValues[ i ];
if ( textFields[ i ] != null ) {
textFields[ i ].setText( string );
}
}
}
}
@Override
public void setSelectedFunction( final FunctionParameterContext context ) {
try {
inSetupUpdate = true;
final FunctionDescription fnDesc = context.getFunction();
//this is empty function?
if ( fnDesc == null ) {
for ( int i = 0; i < textFields.length; i++ ) {
textFields[ i ].dispose();
}
this.textFields = EMPTY_FIELDS;
return;
}
final boolean functionChanged = ( selectedFunction != fnDesc );
this.selectedFunction = fnDesc;
//currently editing one
final String[] parameterValues = context.getParameterValues();
final String[] parameterFieldValues =
getParametersValues( context.getFunctionInformation(), context.getFunction() );
//recreate whole text fields
if ( functionChanged ) {
parameterPane.removeAll();
this.textFields = new TextFieldHolderStruct[ parameterFieldValues.length ];
final int fieldFocus = Math.max( 0, parameterUpdatedCount );
for ( int i = 0; i < parameterFieldValues.length; i++ ) {
this.textFields[ i ] = addTextField( parameterFieldValues[ i ], i, ( i == fieldFocus ) );
}
} else if ( textFields.length != parameterFieldValues.length ) {
final TextFieldHolderStruct[] oldTextFields = this.textFields;
this.textFields = new TextFieldHolderStruct[ parameterFieldValues.length ];
System.arraycopy( oldTextFields, 0, textFields, 0, Math.min( oldTextFields.length, textFields.length ) );
final int fieldFocus = Math.max( 0, parameterUpdatedCount );
for ( int i = parameterFieldValues.length; i < oldTextFields.length; i++ ) {
oldTextFields[ i ].dispose();
}
for ( int i = oldTextFields.length; i < parameterFieldValues.length; i++ ) {
this.textFields[ i ] = addTextField( parameterFieldValues[ i ], i, ( i == fieldFocus ) );
}
}
if ( isMainFormula( context ) == true ) {
updateParameterFields( parameterValues );
//return;
} else {
// If we are in an embedded formula, update the main
// formula's parameter field that is associated with
// this embedded formula.
if ( updateCurrentParameterField( context ) == false ) {
// The parameter field is pointing to the embedded
// formula - update it
updateParameterFields( parameterValues );
}
}
} finally {
inSetupUpdate = false;
invalidate();
revalidate();
repaint();
}
}
private TextFieldHolderStruct addTextField( final String parameterValue,
final int parameterPosition,
final boolean requestFocus ) {
//this value is used to compute field hints.
final int paramPos = Math.max( 0, Math.min( selectedFunction.getParameterCount() - 1, parameterPosition ) );
final String displayName = selectedFunction.getParameterDisplayName( paramPos, Locale.getDefault() );
final String description = selectedFunction.getParameterDescription( paramPos, Locale.getDefault() );
final JLabel paramNameLabel = new JLabel( displayName );
final InlineEditTextField paramTextField = new InlineEditTextField();
paramTextField.setText( parameterValue );
if ( parameterValue != null ) {
paramTextField.setCaretPosition( parameterValue.length() );
}
paramTextField.setFont
( new Font( Font.MONOSPACED, paramTextField.getFont().getStyle(), paramTextField.getFont().getSize() ) );
final FocusListenerHandler handler = new FocusListenerHandler( paramTextField, parameterPosition );
paramTextField.addFocusListener( handler );
if ( requestFocus ) {
paramTextField.setFocusable( true );
paramTextField.requestFocusInWindow();
}
final SelectFieldAction selectFieldAction =
new SelectFieldAction( this, new FieldSelectorUpdateHandler( parameterPosition ), this );
// treat insert field as parameter edit
selectFieldAction.setFocusReturn( paramTextField );
final BorderlessButton button = new BorderlessButton( selectFieldAction );
final TooltipLabel tooltipLabel = new TooltipLabel( description );
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = parameterPosition;
gbc.anchor = GridBagConstraints.WEST;
this.parameterPane.add( paramNameLabel, gbc );
gbc = new GridBagConstraints();
gbc.gridx = 2;
gbc.gridy = parameterPosition;
gbc.anchor = GridBagConstraints.WEST;
gbc.weightx = 1;
gbc.gridwidth = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
this.parameterPane.add( paramTextField, gbc );
gbc = new GridBagConstraints();
gbc.gridx = 3;
gbc.gridy = parameterPosition;
gbc.anchor = GridBagConstraints.WEST;
this.parameterPane.add( button, gbc );
gbc = new GridBagConstraints();
gbc.gridx = 1;
gbc.gridy = parameterPosition;
gbc.anchor = GridBagConstraints.WEST;
gbc.gridwidth = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets( 3, 5, 3, 5 );
this.parameterPane.add( tooltipLabel, gbc );
return new TextFieldHolderStruct( paramTextField, selectFieldAction, handler,
paramNameLabel, button, tooltipLabel );
}
//returns expected number of fields for formula editor
private static int computeFunctionParameterCount( final FunctionInformation info, final FunctionDescription desc ) {
if ( !desc.isInfiniteParameterCount() ) {
return desc.getParameterCount();
}
final String[] parameters = info.getParameters();
int lastNonEmpty = 0;
for ( int i = 0; i < parameters.length; i += 1 ) {
final String p = parameters[ i ];
if ( StringUtils.isEmpty( p ) ) {
continue;
}
lastNonEmpty = i;
}
return Math.max( lastNonEmpty + 1, desc.getParameterCount() ) + FIELDS_ADD;
}
public String[] getParametersValues( final FunctionInformation fnInfo, final FunctionDescription fnDesc ) {
final int paramCount = computeFunctionParameterCount( fnInfo, fnDesc );
final String[] parameterValues = new String[ paramCount ];
final int definedParameterCount = Math.min( fnInfo.getParameterCount(), paramCount );
for ( int i = 0; i < definedParameterCount; i++ ) {
final String text = fnInfo.getParameterText( i );
parameterValues[ i ] = text;
}
//if there is more than FIELDS_MAX_NUMBER parameters -
if ( definedParameterCount > 0 && fnInfo.getParameterCount() > paramCount ) {
final StringBuilder lastParamEatsAllBuffer = new StringBuilder( 100 );
final int lastParamIdx = definedParameterCount - 1;
for ( int i = lastParamIdx; i < fnInfo.getParameterCount(); i++ ) {
if ( i > lastParamIdx ) {
lastParamEatsAllBuffer.append( ';' );
}
lastParamEatsAllBuffer.append( fnInfo.getParameterText( i ) );
}
parameterValues[ lastParamIdx ] = lastParamEatsAllBuffer.toString();
}
return parameterValues;
}
protected TextFieldHolderStruct getParameterField( final int field ) {
return textFields[ field ];
}
public String getParameterValue( final int param ) {
return textFields[ param ].getText();
}
@Override
public void addParameterUpdateListener( final ParameterUpdateListener listener ) {
if ( listenerList.getListenerCount( ParameterUpdateListener.class ) == 0 ) {
listenerList.add( ParameterUpdateListener.class, listener );
}
}
@Override
public void removeParameterUpdateListener( final ParameterUpdateListener listener ) {
listenerList.remove( ParameterUpdateListener.class, listener );
}
protected void fireParameterUpdate( final int param, final String text ) {
final boolean catchAllParameter =
selectedFunction.isInfiniteParameterCount() && ( param >= selectedFunction.getParameterCount() );
final ParameterUpdateListener[] updateListeners = listenerList.getListeners( ParameterUpdateListener.class );
for ( int i = 0; i < updateListeners.length; i++ ) {
final ParameterUpdateListener listener = updateListeners[ i ];
listener.parameterUpdated( new ParameterUpdateEvent( this, param, text, catchAllParameter ) );
}
}
@Override
public void setFields( final FieldDefinition[] fields ) {
this.fields = fields.clone();
}
@Override
public FieldDefinition[] getFields() {
if ( fields == null ) {
return new FieldDefinition[ 0 ];
}
return fields.clone();
}
@Override
public Component getEditorComponent() {
return this;
}
}