/*!
* 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.reporting.designer.core.util;
import java.awt.BorderLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import javax.swing.AbstractAction;
import javax.swing.DefaultComboBoxModel;
import javax.swing.Icon;
import javax.swing.JComboBox;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.EventListenerList;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.table.DefaultTableModel;
import org.apache.commons.lang.ObjectUtils;
import org.pentaho.openformula.ui.FieldDefinition;
import org.pentaho.openformula.ui.FormulaEditorDialog;
import org.pentaho.reporting.designer.core.ReportDesignerContext;
import org.pentaho.reporting.designer.core.editor.ReportDocumentContext;
import org.pentaho.reporting.engine.classic.core.MetaAttributeNames;
import org.pentaho.reporting.engine.classic.core.StaticDataRow;
import org.pentaho.reporting.engine.classic.core.function.GenericExpressionRuntime;
import org.pentaho.reporting.engine.classic.core.function.ReportFormulaContext;
import org.pentaho.reporting.engine.classic.core.layout.output.DefaultProcessingContext;
import org.pentaho.reporting.engine.classic.core.wizard.ContextAwareDataSchemaModel;
import org.pentaho.reporting.engine.classic.core.wizard.DataAttributes;
import org.pentaho.reporting.engine.classic.core.wizard.DataSchema;
import org.pentaho.reporting.engine.classic.core.wizard.DefaultDataAttributeContext;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.pentaho.reporting.libraries.base.util.StringUtils;
import org.pentaho.reporting.libraries.designtime.swing.EllipsisButton;
import org.pentaho.reporting.libraries.designtime.swing.event.DocumentChangeHandler;
import org.pentaho.reporting.libraries.formula.DefaultFormulaContext;
import org.pentaho.reporting.libraries.formula.util.FormulaUtil;
/**
* A text field that acts as a simple input for formulas with a button to invoke the formula editor if needed.
*
* @author Thomas Morgner.
*/
public class FormulaEditorPanel extends JPanel
{
private class OpenFormulaEditorAction extends AbstractAction
{
/**
* Invoked when an action occurs.
*/
public void actionPerformed(final ActionEvent e)
{
final FormulaEditorDialog dialog = GUIUtils.createFormulaEditorDialog(getReportDesignerContext(), FormulaEditorPanel.this);
final String formula = dialog.editFormula(formulaField.getText(), computeFields());
if (formula == null)
{
// cancel pressed ... do nothing ...
return;
}
formulaField.setText(formula);
}
}
private class KeyEventForwarder implements KeyListener
{
private KeyEventForwarder()
{
}
/**
* Invoked when a key has been typed.
* See the class description for {@link java.awt.event.KeyEvent} for a definition of
* a key typed event.
*/
public void keyTyped(final KeyEvent e)
{
final KeyListener[] keyListeners = listenerList.getListeners(KeyListener.class);
for (int i = 0; i < keyListeners.length; i++)
{
final KeyListener keyListener = keyListeners[i];
keyListener.keyTyped(e);
}
}
/**
* Invoked when a key has been pressed.
* See the class description for {@link java.awt.event.KeyEvent} for a definition of
* a key pressed event.
*/
public void keyPressed(final KeyEvent e)
{
final KeyListener[] keyListeners = listenerList.getListeners(KeyListener.class);
for (int i = 0; i < keyListeners.length; i++)
{
final KeyListener keyListener = keyListeners[i];
keyListener.keyPressed(e);
}
}
/**
* Invoked when a key has been released.
* See the class description for {@link java.awt.event.KeyEvent} for a definition of
* a key released event.
*/
public void keyReleased(final KeyEvent e)
{
final KeyListener[] keyListeners = listenerList.getListeners(KeyListener.class);
for (int i = 0; i < keyListeners.length; i++)
{
final KeyListener keyListener = keyListeners[i];
keyListener.keyReleased(e);
}
}
}
private class ValueChangeEventGenerator extends DocumentChangeHandler implements ListDataListener, Runnable
{
private String lastValue;
private ValueChangeEventGenerator()
{
}
protected void handleChange(final DocumentEvent e)
{
SwingUtilities.invokeLater(this);
}
/**
* Sent after the indices in the index0,index1
* interval have been inserted in the data model.
* The new interval includes both index0 and index1.
*
* @param e a <code>ListDataEvent</code> encapsulating the
* event information
*/
public void intervalAdded(final ListDataEvent e)
{
}
/**
* Sent after the indices in the index0,index1 interval
* have been removed from the data model. The interval
* includes both index0 and index1.
*
* @param e a <code>ListDataEvent</code> encapsulating the
* event information
*/
public void intervalRemoved(final ListDataEvent e)
{
}
/**
* Sent when the contents of the list has changed in a way
* that's too complex to characterize with the previous
* methods. For example, this is sent when an item has been
* replaced. Index0 and index1 bracket the change.
*
* @param e a <code>ListDataEvent</code> encapsulating the
* event information
*/
public void contentsChanged(final ListDataEvent e)
{
run();
}
public void run()
{
final String formula = getFormula();
if (ObjectUtilities.equal(lastValue, formula))
{
return;
}
final Object oldValue = lastValue;
lastValue = formula;
firePropertyChange("formula", oldValue, lastValue);
}
}
private static class GenericDataFieldDefinition implements FieldDefinition
{
private String name;
private GenericDataFieldDefinition(final String name)
{
this.name = name;
}
public String getName()
{
return name;
}
public String getDisplayName()
{
return name;
}
public Icon getIcon()
{
return IconLoader.getInstance().getDataSetsIcon();
}
public Class getFieldType()
{
return Object.class;
}
}
private static final FieldDefinition[] EMPTY_FIELDS = new FieldDefinition[0];
private DefaultDataAttributeContext dataAttributeContext;
private ReportDesignerContext reportDesignerContext;
private boolean formulaFragment;
private EllipsisButton ellipsisButton;
private EventListenerList listenerList;
private JTextField formulaField;
private JComboBox formulaBox;
private DefaultComboBoxModel tagModel;
private boolean comboBoxActive;
private boolean limitFields;
private ValueChangeEventGenerator changeEventGenerator;
private FormulaEditorDataModel editorDataModel;
/**
* Creates a new <code>JPanel</code> with a double buffer
* and a flow layout.
*/
public FormulaEditorPanel()
{
this.listenerList = new EventListenerList();
this.dataAttributeContext = new DefaultDataAttributeContext();
setLayout(new BorderLayout());
ellipsisButton = new EllipsisButton("...");
ellipsisButton.setDefaultCapable(false);
ellipsisButton.setMargin(new Insets(0, 0, 0, 0));
ellipsisButton.addActionListener(new OpenFormulaEditorAction());
changeEventGenerator = new ValueChangeEventGenerator();
tagModel = new DefaultComboBoxModel();
tagModel.addListDataListener(changeEventGenerator);
formulaBox = new JComboBox(tagModel);
formulaBox.setEditable(true);
formulaBox.addKeyListener(new KeyEventForwarder());
formulaField = new JTextField();
formulaField.getDocument().addDocumentListener(changeEventGenerator);
formulaBox.addKeyListener(new KeyEventForwarder());
add(formulaField, BorderLayout.CENTER);
add(ellipsisButton, BorderLayout.EAST);
}
private void activateComboBox()
{
if (comboBoxActive)
{
return;
}
final String formula = getFormula();
remove(formulaField);
add(formulaBox, BorderLayout.CENTER);
formulaBox.setSelectedItem(formula);
comboBoxActive = true;
revalidate();
}
private void activateTextField()
{
if (comboBoxActive == false)
{
return;
}
final String formula = getFormula();
remove(formulaBox);
add(formulaField, BorderLayout.CENTER);
formulaField.setText(formula);
comboBoxActive = false;
revalidate();
}
public ReportDesignerContext getReportDesignerContext()
{
return reportDesignerContext;
}
public void setReportDesignerContext(final ReportDesignerContext reportDesignerContext)
{
this.reportDesignerContext = reportDesignerContext;
}
protected FieldDefinition[] computeFields()
{
if (reportDesignerContext == null)
{
return EMPTY_FIELDS;
}
final ReportDocumentContext renderContext = reportDesignerContext.getActiveContext();
if (renderContext == null)
{
return EMPTY_FIELDS;
}
final ContextAwareDataSchemaModel model = renderContext.getReportDataSchemaModel();
final String[] columnNames = model.getColumnNames();
final ArrayList<FieldDefinition> fields = new ArrayList<FieldDefinition>(columnNames.length);
final DataSchema dataSchema = model.getDataSchema();
final DefaultDataAttributeContext attributeContext = new DefaultDataAttributeContext();
final String parameter;
if (editorDataModel != null)
{
parameter = editorDataModel.getParameter();
}
else
{
parameter = null;
}
for (int i = 0; i < columnNames.length; i++)
{
final String columnName = columnNames[i];
final DataAttributes attributes = dataSchema.getAttributes(columnName);
if (attributes == null)
{
throw new IllegalStateException("No data-schema for expression with name '" + columnName + '\'');
}
final Object source = attributes.getMetaAttribute
(MetaAttributeNames.Core.NAMESPACE, MetaAttributeNames.Core.SOURCE, String.class, attributeContext);
if (limitFields == false ||
MetaAttributeNames.Core.SOURCE_VALUE_PARAMETER.equals(source) ||
MetaAttributeNames.Core.SOURCE_VALUE_ENVIRONMENT.equals(source))
{
if (limitFields && "report.date".equals(columnName))
{
// this is a magical field
continue;
}
fields.add(new DataSchemaFieldDefinition(columnName, attributes, dataAttributeContext));
if (ObjectUtils.equals(parameter, columnName))
{
break;
}
}
}
if (editorDataModel != null)
{
final String[] dataFields = editorDataModel.getDataFields();
for (final String field : dataFields)
{
fields.add(new GenericDataFieldDefinition(field));
}
}
return fields.toArray(new FieldDefinition[fields.size()]);
}
public boolean isLimitFields()
{
return limitFields;
}
public void setLimitFields(final boolean limitFields)
{
this.limitFields = limitFields;
}
public boolean isFormulaFragment()
{
return formulaFragment;
}
public void setFormulaFragment(final boolean formulaFragment)
{
this.formulaFragment = formulaFragment;
}
public String getFormula()
{
final String s;
if (comboBoxActive)
{
s = (String) formulaBox.getSelectedItem();
}
else
{
s = formulaField.getText();
}
if (StringUtils.isEmpty(s, true))
{
return null;
}
if (isFormulaFragment())
{
return FormulaUtil.createFormulaFromUIText(s);
}
return s;
}
public void setFormula(final String text)
{
if (text == null)
{
formulaField.setText(null);
formulaBox.setSelectedItem(null);
return;
}
if (isFormulaFragment())
{
final GenericExpressionRuntime expressionRuntime = new GenericExpressionRuntime
(new StaticDataRow(), new DefaultTableModel(), -1, new DefaultProcessingContext());
final String formulaText = FormulaUtil.createEditorTextFromFormula
(text, new ReportFormulaContext(new DefaultFormulaContext(), expressionRuntime));
formulaField.setText(formulaText);
formulaBox.setSelectedItem(formulaText);
}
else
{
formulaField.setText(text);
formulaBox.setSelectedItem(text);
}
}
/**
* Sets whether or not this component is enabled.
* A component that is enabled may respond to user input,
* while a component that is not enabled cannot respond to
* user input. Some components may alter their visual
* representation when they are disabled in order to
* provide feedback to the user that they cannot take input.
* <p>Note: Disabling a component does not disable its children.
* <p/>
* <p>Note: Disabling a lightweight component does not prevent it from
* receiving MouseEvents.
*
* @param enabled true if this component should be enabled, false otherwise
* @beaninfo preferred: true
* bound: true
* attribute: visualUpdate true
* description: The enabled state of the component.
* @see java.awt.Component#isEnabled
* @see java.awt.Component#isLightweight
*/
public void setEnabled(final boolean enabled)
{
super.setEnabled(enabled);
formulaField.setEnabled(enabled);
ellipsisButton.setEnabled(enabled);
}
public void selectAll()
{
formulaField.selectAll();
}
public void setEditable(final boolean editable)
{
formulaField.setEditable(editable);
ellipsisButton.setEnabled(editable);
}
public boolean isEditable()
{
return formulaField.isEditable();
}
public void addFormulaKeyListener(final KeyListener k)
{
this.listenerList.add(KeyListener.class, k);
}
public void removeFormulaKeyListener(final KeyListener k)
{
this.listenerList.remove(KeyListener.class, k);
}
public String[] getTags()
{
final int size = tagModel.getSize();
final String[] retval = new String[size];
for (int i = 0; i < retval.length; i++)
{
retval[i] = (String) tagModel.getElementAt(i);
}
return retval;
}
public void setTags(final String[] tags)
{
if (tags == null)
{
throw new NullPointerException();
}
tagModel.removeAllElements();
for (int i = 0; i < tags.length; i++)
{
tagModel.addElement(tags[i]);
}
if (tags.length == 0)
{
activateTextField();
}
else
{
activateComboBox();
}
}
public FormulaEditorDataModel getEditorDataModel()
{
return editorDataModel;
}
public void setEditorDataModel(final FormulaEditorDataModel editorDataModel)
{
this.editorDataModel = editorDataModel;
}
}