/* * RapidMiner * * Copyright (C) 2001-2011 by Rapid-I and the contributors * * Complete list of developers available at our web site: * * http://rapid-i.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.properties; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Vector; import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.ListSelectionModel; import javax.swing.ScrollPaneConstants; import javax.swing.SwingConstants; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.metadata.AttributeMetaData; import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; import com.rapidminer.operator.ports.metadata.ModelMetaData; import com.rapidminer.parameter.ParameterTypeExpression; import com.rapidminer.tools.math.function.ExpressionParser; import com.rapidminer.tools.math.function.FunctionDescription; public class ExpressionPropertyDialog extends PropertyDialog { private static final long serialVersionUID = 5567661137372752202L; private static final int FUNCTION_ROW_LENGTH = 4; private JTextField currentExpression = new JTextField(); private static final String OK_ICON_NAME = "ok.png"; private static final String ERROR_ICON_NAME = "error.png"; private static Icon OK_ICON = null; private static Icon ERROR_ICON = null; static { OK_ICON = SwingTools.createIcon("16/" + OK_ICON_NAME); ERROR_ICON = SwingTools.createIcon("16/" + ERROR_ICON_NAME); } private JLabel validationLabel = new JLabel(); private JLabel validationIcon = new JLabel(ERROR_ICON); private ExpressionParser parser = new ExpressionParser(true); private JScrollPane functionButtonScrollPane; private JPanel functionsButtonsPanel = new JPanel(); private GridBagLayout functionButtonsLayout = new GridBagLayout(); private GridBagConstraints functionButtonsC = new GridBagConstraints(); public ExpressionPropertyDialog(final ParameterTypeExpression type, String initialValue) { super(type, "expression"); final Vector<String> knownAttributes = new Vector<String>(); InputPort inPort = ((ParameterTypeExpression)getParameterType()).getInputPort(); if (inPort != null) { if (inPort.getMetaData() instanceof ExampleSetMetaData) { ExampleSetMetaData emd = (ExampleSetMetaData) inPort.getMetaData(); for (AttributeMetaData amd : emd.getAllAttributes()) { knownAttributes.add(amd.getName()); } } else if (inPort.getMetaData() instanceof ModelMetaData) { ModelMetaData mmd = (ModelMetaData) inPort.getMetaData(); if (mmd != null) { ExampleSetMetaData emd = mmd.getTrainingSetMetaData(); if (emd != null) { for (AttributeMetaData amd : emd.getAllAttributes()) { knownAttributes.add(amd.getName()); } } } } } Collections.sort(knownAttributes); Collection<AbstractButton> buttons = new LinkedList<AbstractButton>(); buttons.add(makeOkButton()); buttons.add(makeCancelButton()); JPanel mainPanel = new JPanel(); GridBagLayout mainLayout = new GridBagLayout(); mainPanel.setLayout(mainLayout); GridBagConstraints mainC = new GridBagConstraints(); mainC.fill = GridBagConstraints.BOTH; mainC.weightx = 1; mainC.weighty = 0; mainC.insets = new Insets(7, 7, 7, 7); JPanel expressionPanel = new JPanel(); GridBagLayout expressionLayout = new GridBagLayout(); expressionPanel.setLayout(expressionLayout); GridBagConstraints expressionC = new GridBagConstraints(); expressionC.fill = GridBagConstraints.BOTH; expressionC.insets = new Insets(7, 7, 7, 7); expressionC.weightx = 0; expressionC.weighty = 0; JLabel label = new JLabel("Expression:"); expressionLayout.setConstraints(label, expressionC); expressionPanel.add(label); currentExpression.setPreferredSize(new Dimension(200, 23)); currentExpression.addKeyListener(new KeyListener() { @Override public void keyTyped(KeyEvent e) {} @Override public void keyPressed(KeyEvent e) {} @Override public void keyReleased(KeyEvent e) { validateExpression(); } }); expressionC.weightx = 1; expressionC.gridwidth = GridBagConstraints.REMAINDER; expressionLayout.setConstraints(currentExpression, expressionC); expressionPanel.add(currentExpression); expressionC.weightx = 0; expressionC.gridwidth = GridBagConstraints.RELATIVE; expressionLayout.setConstraints(validationIcon, expressionC); expressionPanel.add(validationIcon); Dimension dimension200to30 = new Dimension(200, 30); validationLabel.setPreferredSize(dimension200to30); validationLabel.setMinimumSize(dimension200to30); validationLabel.setMaximumSize(dimension200to30); validationLabel.setAlignmentX(SwingConstants.TOP); expressionC.weightx = 1; expressionC.gridwidth = GridBagConstraints.REMAINDER; expressionLayout.setConstraints(validationLabel, expressionC); expressionPanel.add(validationLabel); expressionC.weightx = 0; expressionC.gridwidth = GridBagConstraints.RELATIVE; JLabel hiddenLabel = new JLabel(); expressionLayout.setConstraints(hiddenLabel, expressionC); expressionPanel.add(hiddenLabel); final JCheckBox allowUndeclaredBox = new JCheckBox("Allow Unknown?", false); allowUndeclaredBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { setAllowUndeclared(allowUndeclaredBox.isSelected()); } }); setAllowUndeclared(false); expressionC.weightx = 0; expressionC.anchor = GridBagConstraints.WEST; expressionC.gridwidth = GridBagConstraints.REMAINDER; expressionLayout.setConstraints(allowUndeclaredBox, expressionC); expressionPanel.add(allowUndeclaredBox); mainC.gridwidth = GridBagConstraints.REMAINDER; mainLayout.setConstraints(expressionPanel, mainC); mainPanel.add(expressionPanel); JPanel functionPanel = new JPanel(); GridBagLayout functionsLayout = new GridBagLayout(); functionPanel.setLayout(functionsLayout); functionPanel.setBorder(BorderFactory.createTitledBorder("Functions")); GridBagConstraints functionsC = new GridBagConstraints(); functionsC.fill = GridBagConstraints.BOTH; functionsC.insets = new Insets(7, 7, 7, 7); functionsC.weightx = 0; functionsC.weighty = 0; label = new JLabel("Type:"); functionsLayout.setConstraints(label, functionsC); functionPanel.add(label); final JComboBox functionsTypeBox = new JComboBox(parser.getFunctionGroups()); functionsTypeBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { updateFunctions((String)functionsTypeBox.getSelectedItem()); } }); functionsC.gridwidth = GridBagConstraints.REMAINDER; functionsC.weightx = 1; functionsLayout.setConstraints(functionsTypeBox, functionsC); functionPanel.add(functionsTypeBox); functionsButtonsPanel.setLayout(functionButtonsLayout); functionButtonsC.fill = GridBagConstraints.BOTH; functionButtonsC.anchor = GridBagConstraints.NORTHWEST; functionButtonsC.insets = new Insets(7, 7, 7, 7); functionButtonsC.weightx = 1; functionButtonsC.weighty = 0; updateFunctions(parser.getFunctionGroups()[0]); JPanel outerButtonPanel = new JPanel(); outerButtonPanel.setLayout(new GridBagLayout()); GridBagConstraints outerButtonC = new GridBagConstraints(); outerButtonC.gridwidth = GridBagConstraints.REMAINDER; outerButtonC.fill = GridBagConstraints.HORIZONTAL; outerButtonC.weightx = 1; outerButtonC.weighty = 1; outerButtonC.anchor = GridBagConstraints.NORTHWEST; outerButtonPanel.add(functionsButtonsPanel, outerButtonC); outerButtonC.weighty = 1; outerButtonC.fill = GridBagConstraints.BOTH; outerButtonPanel.add(new JPanel(), outerButtonC); functionsC.weightx = 0; functionsC.weighty = 1; functionsC.anchor = GridBagConstraints.NORTH; functionButtonScrollPane = new JScrollPane(outerButtonPanel); functionButtonScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); functionsLayout.setConstraints(functionButtonScrollPane, functionsC); functionPanel.add(functionButtonScrollPane); JPanel attributesPanel = new JPanel(); GridBagLayout attributesLayout = new GridBagLayout(); attributesPanel.setLayout(attributesLayout); GridBagConstraints attributesC = new GridBagConstraints(); attributesC.fill = GridBagConstraints.BOTH; attributesC.insets = new Insets(7, 7, 7, 7); attributesC.weightx = 1; attributesC.weighty = 1; final JList attributeList = new JList(knownAttributes); attributeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); attributeList.addMouseListener(new MouseListener() { @Override public void mouseReleased(MouseEvent e) {} @Override public void mousePressed(MouseEvent e) {} @Override public void mouseExited(MouseEvent e) {} @Override public void mouseEntered(MouseEvent e) {} @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() >= 2) { addToExpression(attributeList.getSelectedValue().toString()); } } }); attributesC.gridwidth = GridBagConstraints.REMAINDER; attributesLayout.setConstraints(attributeList, attributesC); attributesPanel.add(attributeList); mainC.weighty = 1; mainC.weightx = 0.6; mainC.gridwidth = GridBagConstraints.RELATIVE; mainLayout.setConstraints(functionPanel, mainC); mainPanel.add(functionPanel); mainC.weightx = 0.4; JScrollPane attributeScrollPane = new JScrollPane(attributesPanel); attributeScrollPane.setBorder(BorderFactory.createTitledBorder("Attributes")); mainLayout.setConstraints(attributeScrollPane, mainC); mainPanel.add(attributeScrollPane); layoutDefault(mainPanel, NORMAL, buttons.toArray(new AbstractButton[buttons.size()])); if (initialValue != null) currentExpression.setText(initialValue); validateExpression(); setSize(850, 675); } private void setAllowUndeclared(boolean allowUndeclared) { parser.initParser(true); if (allowUndeclared) { parser.getParser().setAllowUndeclared(true); } else { InputPort inPort = ((ParameterTypeExpression)getParameterType()).getInputPort(); if (inPort != null) { if (inPort.getMetaData() instanceof ExampleSetMetaData) { ExampleSetMetaData emd = (ExampleSetMetaData) inPort.getMetaData(); for (AttributeMetaData amd : emd.getAllAttributes()) { if (amd.isNominal()) { parser.getParser().addVariable(amd.getName(), ""); } else { parser.getParser().addVariable(amd.getName(), Double.NaN); } } } else if (inPort.getMetaData() instanceof ModelMetaData) { ModelMetaData mmd = (ModelMetaData) inPort.getMetaData(); if (mmd != null) { ExampleSetMetaData emd = mmd.getTrainingSetMetaData(); if (emd != null) { for (AttributeMetaData amd : emd.getAllAttributes()) { if (amd.isNominal()) { parser.getParser().addVariable(amd.getName(), ""); } else { parser.getParser().addVariable(amd.getName(), Double.NaN); } } } } } } } validateExpression(); requestExpressionFocus(); } private void addToExpression(String value) { if (value == null) return; String selectedText = currentExpression.getSelectedText(); if ((selectedText != null) && (selectedText.length() > 0)) { // replace selected text by function including the selection as argument (if the string to be added actually IS a function...) if (value.endsWith("()")) { int selectionStart = currentExpression.getSelectionStart(); int selectionEnd = currentExpression.getSelectionEnd(); String text = currentExpression.getText(); String firstPart = text.substring(0, selectionStart); String lastPart = text.substring(selectionEnd); currentExpression.setText(firstPart + value + lastPart); int lengthForCaretPosition = value.length(); if (value.endsWith("()")) { lengthForCaretPosition--; } currentExpression.setCaretPosition(selectionStart + lengthForCaretPosition); addToExpression(selectedText); currentExpression.setCaretPosition(currentExpression.getCaretPosition() + 1); validateExpression(); requestExpressionFocus(); } else { int selectionStart = currentExpression.getSelectionStart(); int selectionEnd = currentExpression.getSelectionEnd(); String text = currentExpression.getText(); String firstPart = text.substring(0, selectionStart); String lastPart = text.substring(selectionEnd); currentExpression.setText(firstPart + value + lastPart); int lengthForCaretPosition = value.length(); if (value.endsWith("()")) { lengthForCaretPosition--; } currentExpression.setCaretPosition(selectionStart + lengthForCaretPosition); validateExpression(); requestExpressionFocus(); } } else { // just add the text at the current caret position int caretPosition = currentExpression.getCaretPosition(); String text = currentExpression.getText(); if ((text != null) && (text.length() > 0)) { String firstPart = text.substring(0, caretPosition); String lastPart = text.substring(caretPosition); currentExpression.setText(firstPart + value + lastPart); int lengthForCaretPosition = value.length(); if (value.endsWith("()")) { lengthForCaretPosition--; } currentExpression.setCaretPosition(caretPosition + lengthForCaretPosition); } else { currentExpression.setText(value); int lengthForCaretPosition = value.length(); if (value.endsWith("()")) { lengthForCaretPosition--; } currentExpression.setCaretPosition(caretPosition + lengthForCaretPosition); currentExpression.setCaretPosition(lengthForCaretPosition); } validateExpression(); requestExpressionFocus(); } } private void requestExpressionFocus() { currentExpression.requestFocusInWindow(); } private void updateFunctions(String functionGroup) { // hack to prevent extremely wide rows due to long function names // future function groups may need to be added as well int funtionRowLength = FUNCTION_ROW_LENGTH; if (functionGroup.equals("Date")) { funtionRowLength -= 1; } functionsButtonsPanel.removeAll(); List<FunctionDescription> functions = parser.getFunctions(functionGroup); functionButtonsC.gridwidth = 1; // dummy components so buttons always start at the top-left corner for (int i=0; i<funtionRowLength-1; i++) { functionsButtonsPanel.add(new JLabel(), functionButtonsC); } functionButtonsC.gridwidth = GridBagConstraints.REMAINDER; functionsButtonsPanel.add(new JLabel(), functionButtonsC); functionButtonsC.gridwidth = 1; int index = 1; for (final FunctionDescription currentFunction : functions) { JButton currentButton = new JButton(); currentButton.setText(currentFunction.getFunction()); String argumentString = null; int numberOfArguments = currentFunction.getNumberOfArguments(); if (numberOfArguments == FunctionDescription.UNLIMITED_NUMBER_OF_ARGUMENTS) { argumentString = "unlimited arguments"; } else if (numberOfArguments == 1) { argumentString = "1 argument"; } else { argumentString = numberOfArguments + " arguments"; } currentButton.setToolTipText("<html><b>" + currentFunction.getName() + "</b>: " + currentFunction.getDescription() + " (<i>" + argumentString + "</i>)</html>"); currentButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { addToExpression(currentFunction.getFunction()); } }); functionButtonsLayout.setConstraints(currentButton, functionButtonsC); functionsButtonsPanel.add(currentButton); index++; if (index % funtionRowLength == 0) { index = 0; functionButtonsC.gridwidth = GridBagConstraints.REMAINDER; } else { functionButtonsC.gridwidth = 1; } } functionsButtonsPanel.revalidate(); requestExpressionFocus(); } private void validateExpression() { String expression = currentExpression.getText(); if (expression != null) { if (expression.length() > 0) { parser.getParser().parseExpression(expression); if (parser.getParser().hasError()) { validationIcon.setIcon(ERROR_ICON); validationLabel.setText("<html><b>Error: </b>" + parser.getParser().getErrorInfo() + "</html>"); return; } else { validationIcon.setIcon(OK_ICON); validationLabel.setText("Expression is syntactically correct."); } } else { validationIcon.setIcon(ERROR_ICON); validationLabel.setText("<html><b>Warning: </b>Please specify a valid expression.</html>"); } } else { validationIcon.setIcon(ERROR_ICON); validationLabel.setText("<html><b>Warning: </b>Please specify a valid expression.</html>"); } } public String getExpression() { return currentExpression.getText(); } @Override protected String getInfoText() { //Hint 1: no function symbols (parentheses etc.) are allowed. //Hint 2: validation may not work if unknown attributes or macros are included return "<html><p>" + "Please specify a valid expression in this dialog. Expressions can consist of numbers, text, constants (like Pi, e true, or false), functions, and variable names. " + "Fractions in numbers are indicated by '.' and nominal text has to be quoted with double quotes. The possible functions can be selected by clicking on them below. " + "Variables can be attribute names (select them by double clicking below or type in the name) or macros. " + "Hence, used attribute names are not allowed to contain parentheses or other function symbols. " + "All changes will result in a validation check. The icon on the right shows if the check was successful, a tool tip gives you more information." + "</p></html>"; } }