/*! * 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 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; import javax.swing.*; 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 java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.ArrayList; /** * 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; } }