/*
* 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 java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Locale;
import javax.swing.Box;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.border.EmptyBorder;
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;
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;
}
}