/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.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.Color; import java.awt.Dimension; import java.awt.Font; 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.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.ScrollPaneConstants; import javax.swing.SwingConstants; import javax.swing.UIManager; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.text.BadLocationException; import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.Style; import org.fife.ui.rsyntaxtextarea.SyntaxScheme; import org.fife.ui.rsyntaxtextarea.Token; import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; import org.fife.ui.rtextarea.Gutter; import org.fife.ui.rtextarea.RTextScrollPane; import org.jdesktop.swingx.JXTaskPane; import com.rapidminer.Process; import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.tools.ExtendedJScrollPane; import com.rapidminer.gui.tools.FilterListener; import com.rapidminer.gui.tools.FilterTextField; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.ScrollableJPopupMenu; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.TextFieldWithAction; import com.rapidminer.gui.tools.syntax.ExpressionTokenMaker; import com.rapidminer.operator.Operator; import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; import com.rapidminer.operator.ports.metadata.ModelMetaData; import com.rapidminer.parameter.ParameterTypeExpression; import com.rapidminer.tools.I18N; import com.rapidminer.tools.Observable; import com.rapidminer.tools.Observer; import com.rapidminer.tools.expression.ExampleResolver; import com.rapidminer.tools.expression.ExpressionException; import com.rapidminer.tools.expression.ExpressionParser; import com.rapidminer.tools.expression.ExpressionParserBuilder; import com.rapidminer.tools.expression.ExpressionRegistry; import com.rapidminer.tools.expression.FunctionDescription; import com.rapidminer.tools.expression.FunctionInput; import com.rapidminer.tools.expression.FunctionInput.Category; import com.rapidminer.tools.expression.MacroResolver; /** * * The {@link ExpressionPropertyDialog} enables to enter an expression out of functions, attribute * values, constants and macro values and validates the expression's syntax. * * @author Ingo Mierswa, Marco Boeck, Sabrina Kirstein * */ public class ExpressionPropertyDialog extends PropertyDialog { private static final long serialVersionUID = 5567661137372752202L; /** * An input panel owns an {@link Observer}, which is updated about model changes. * * @author Sabrina Kirstein */ private class PrivateInputObserver implements Observer<FunctionInputPanel> { @Override public void update(Observable<FunctionInputPanel> observable, FunctionInputPanel arg) { if (arg != null) { // add the function name to the expression if (arg.getCategory() == Category.SCOPE) { boolean predefined = false; for (String predefinedMacro : controllingProcess.getMacroHandler() .getAllGraphicallySupportedPredefinedMacros()) { // if this is a predefined macro if (predefinedMacro.equals(arg.getInputName())) { // if the old expression parser is supported if (parser.getExpressionContext().getFunction("macro") != null) { // if the predefined macro is the number applied times, evaluate it if (predefinedMacro .equals(Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES_USER_FRIENDLY) || predefinedMacro.equals(Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES)) { addToExpression("%{" + arg.getInputName() + "}"); // otherwise show the string } else { addToExpression("macro(\"" + arg.getInputName() + "\")"); } } else { // if the predefined macro is the number applied times, evaluate it if (predefinedMacro .equals(Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES_USER_FRIENDLY) || predefinedMacro.equals(Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES)) { addToExpression("eval(%{" + arg.getInputName() + "})"); // otherwise show the string } else { addToExpression("%{" + arg.getInputName() + "}"); } } predefined = true; break; } } // if the macro is a custom macro, give the user the choice between adding the // value or an evaluated expression (only if "macro" and/or "eval" functions // available in the dialog context) if (!predefined) { if (parser.getExpressionContext().getFunction("macro") != null || parser.getExpressionContext().getFunction("eval") != null) { MacroSelectionDialog macroSelectionDialog = new MacroSelectionDialog(arg, parser .getExpressionContext().getFunction("macro") != null); macroSelectionDialog.setLocation(arg.getLocationOnScreen().x, arg.getLocationOnScreen().y + 40); macroSelectionDialog.setVisible(true); addToExpression(macroSelectionDialog.getExpression()); } else { addToExpression("%{" + arg.getInputName() + "}"); } } } else if (arg.getCategory() == Category.DYNAMIC) { if (parser.getExpressionContext().getConstant(arg.getInputName()) != null) { // if the attribute has the same name as a constant, add it with the // brackets addToExpression("[" + arg.getInputName() + "]"); } else if (arg.getInputName().matches("(^[A-Za-z])([A-Z_a-z\\d]*)")) { // check whether the attribute is alphanumerical without a number at the // front addToExpression(arg.getInputName()); } else { // if the attribute is not alphanumeric, add it with the brackets, // escape [ , ] and \ String inputName = arg.getInputName().replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]"); addToExpression("[" + inputName + "]"); } } else { addToExpression(arg.getInputName()); } } else { // the filtered model changed: // update the panels in regard to the filtered model updateInputs(); } } } /** * {@link FunctionDescriptionPanel} owns an {@link Observer}, which is updated on model changes. * * @author Sabrina Kirstein */ private class PrivateModelObserver implements Observer<FunctionDescription> { @Override public void update(Observable<FunctionDescription> observable, FunctionDescription arg) { if (arg != null) { // add the function name to the expression addToExpression(arg.getDisplayName(), arg); } else { // the filtered model changed: // update the panels in regard to the filtered model updateFunctions(); } } } /** * * Mouse listener to react on hover events of the filter menu button. Highlights the button, * when hovered. * * @author Sabrina Kirstein * */ private final class HoverBorderMouseListener extends MouseAdapter { private final JButton button; public HoverBorderMouseListener(final JButton pageButton) { this.button = pageButton; } @Override public void mouseReleased(final MouseEvent e) { if (!button.isEnabled()) { button.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); } super.mouseReleased(e); } @Override public void mouseExited(final MouseEvent e) { button.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); super.mouseExited(e); } @Override public void mouseEntered(final MouseEvent e) { if (button.isEnabled()) { button.setBorder(BorderFactory.createLineBorder(SwingTools.RAPIDMINER_ORANGE, 1)); } super.mouseEntered(e); } } // EXPRESSION /** text area with the highlighted current expression */ private RSyntaxTextArea currentExpression = new RSyntaxTextArea(); /** scroll pane of the text area with the highlighted current expression */ private RTextScrollPane scrollPaneExpression = new RTextScrollPane(); // PARSER private final ExpressionParser parser; private final com.rapidminer.Process controllingProcess; // INPUTS /** search text filter of the inputs field */ private final FilterTextField inputsFilterField = new FilterTextField(12); /** QuickFilter: Nominal */ private JCheckBox chbNominal; /** QuickFilter: Numeric */ private JCheckBox chbNumeric; /** QuickFilter: Date_time */ private JCheckBox chbDateTime; /** the input categorization panes */ private Map<String, JXTaskPane> inputCategoryTaskPanes; /** model of the inputs */ private FunctionInputsModel inputsModel = new FunctionInputsModel(); /** observer to update the GUI, if the model changed or an input value was clicked */ private PrivateInputObserver inputObserver = new PrivateInputObserver(); /** listener which checks if the value of the filter string changed */ private FilterListener filterInputsListener = new FilterListener() { @Override public void valueChanged(String value) { // gives the filter string to the model inputsModel.setFilterNameString(value); } }; /** action which is executed when the filter search field is cleared */ private transient final ResourceAction clearInputsFilterAction = new ResourceAction(true, "clear_filter") { private static final long serialVersionUID = 3236281211064051583L; @Override public void actionPerformed(final ActionEvent e) { inputsFilterField.clearFilter(); inputsModel.setFilterNameString(""); inputsFilterField.requestFocusInWindow(); } }; /** panel containing this {@link FunctionInput}s part */ private JPanel inputsPanel = new JPanel(); /** layout of the inputs part */ private GridBagLayout inputsLayout; // FUNCTIONS /** search text field in the {@link FunctionDescription}s part */ private final FilterTextField functionsFilterField = new FilterTextField(12); /** the function categorization panes */ private Map<String, JXTaskPane> functionCategoryTaskPanes; /** model of the {@link FunctionDescription}s */ private FunctionDescriptionModel functionModel = new FunctionDescriptionModel(); /** observer to update the GUI, if the model changed or a function was clicked */ private PrivateModelObserver functionObserver = new PrivateModelObserver(); /** listener which checks if the value of the filter string changed */ private FilterListener filterFunctionsListener = new FilterListener() { @Override public void valueChanged(String value) { functionModel.setFilterNameString(value); } }; /** action which is executed when the filter search field is cleared */ private transient final ResourceAction clearFunctionsFilterAction = new ResourceAction(true, "clear_filter") { private static final long serialVersionUID = 3236281211064051583L; @Override public void actionPerformed(final ActionEvent e) { functionsFilterField.clearFilter(); functionModel.setFilterNameString(""); functionsFilterField.requestFocusInWindow(); } }; /** scroll pane containing the different {@link FunctionDescriptionPanel}s */ private JScrollPane functionButtonScrollPane; /** panel containing the {@link FunctionDescription}s part */ private JPanel functionsPanel = new JPanel(); /** layout of the functions part */ private GridBagLayout functionButtonsLayout = new GridBagLayout(); /** * hack to prevent filter popup (inputs part) from opening itself again when you click the * button to actually close it while it is open */ private long lastPopupCloseTime; /** syntax highlighting color */ private static final Color DARK_PURPLE = new Color(139, 0, 139); /** syntax highlighting color */ private static final Color DARK_CYAN = new Color(0, 139, 139); /** color of the validation label, when no error occured */ private static final Color DARK_GREEN = new Color(45, 136, 45); /** the background color of the lists with functions */ private static final Color LIGHTER_GRAY = Colors.WINDOW_BACKGROUND; /** Color of the expression border */ private static final Color COLOR_BORDER_EXPRESSION = Colors.TEXTFIELD_BORDER; /** Color of the category title {@link JXTaskPane}s */ private static final Color COLOR_TITLE_TASKPANE_BACKGROUND = new Color(230, 230, 230); /** icon to of the search text field, which is shown when the clear filter icon is hovered */ private static final ImageIcon CLEAR_FILTER_HOVERED_ICON = SwingTools.createIcon("16/x-mark_orange.png"); /** maximal number of {@link FunctionInput}s that is shown in open {@link JXTaskPane}(s) */ private static final int MAX_NMBR_INPUTS_SHOWN = 7; /** * maximal number of {@link FunctionDescription}s that is shown in open {@link JXTaskPane}(s) */ private static final int MAX_NMBR_FUNCTIONS_SHOWN = 15; /** size of the expression */ private static final int NUMBER_OF_EXPRESSION_ROWS = 6; /** message when the search in the model resulted in an empty set */ private static final String MESSAGE_NO_RESULTS = " No search results found."; /** Icon name of the error icon (parser) */ private static final String ERROR_ICON_NAME = "error.png"; /** The font size of the categories({@link JXTaskPane}s) titles */ private static final String FONT_SIZE_HEADER = "5"; /** maximal width of the inputs panel part */ private static final int WIDTH_ATTRIBUTE_PANEL = 350; /** width of the search field in the inputs part */ private static final int WIDTH_INPUTS_SEARCH_FIELD = 200; /** width of the search field in the functions part */ private static final int WIDTH_FUNCTION_SEARCH_FIELD = 300; /** height of the expression field */ private static final int HEIGHT_EXPRESSION_SCROLL_PANE = 120; /** height of the search fields */ private static final int HEIGHT_SEARCH_FIELD = 24; /** standard padding size */ private static final int STD_INSET_GBC = 7; /** error icon (parser) */ private static Icon ERROR_ICON = null; static { ERROR_ICON = SwingTools.createIcon("13/" + ERROR_ICON_NAME); } /** label showing a validation result of the expression */ private JLabel validationLabel = new JLabel(); private JTextArea validationTextArea = new JTextArea(); /** typical filter icon */ private static final ImageIcon ICON_FILTER = SwingTools.createIcon("16/" + "funnel.png"); /** * Creates an {@link ExpressionPropertyDialog} with the given initial value. * * @param type * @param initialValue */ public ExpressionPropertyDialog(final ParameterTypeExpression type, String initialValue) { this(type, null, initialValue); } /** * Creates an {@link ExpressionPropertyDialog} with the given initial value, controlling * process, expression parser, input model and function model * * @param type * @param process * @param inputs * @param functions * @param parser * @param initialValue */ public ExpressionPropertyDialog(ParameterTypeExpression type, Process process, List<FunctionInput> inputs, List<FunctionDescription> functions, ExpressionParser parser, String initialValue) { super(type, "expression"); this.inputsModel.addObserver(inputObserver, false); this.functionModel.addObserver(functionObserver, false); this.controllingProcess = getControllingProcessOrNull(type, process); this.inputsModel.addContent(inputs); this.functionModel.addContent(functions); this.parser = parser; ExpressionTokenMaker.removeFunctionInputs(); ExpressionTokenMaker.addFunctionInputs(inputs); ExpressionTokenMaker.addFunctions(functions); initGui(initialValue); FunctionDescriptionPanel.updateMaximalWidth(functionsPanel.getSize().width); updateFunctions(); } /** * Creates an {@link ExpressionPropertyDialog} with the given initial value and a controlling * process * * @param type * @param process * @param initialValue */ public ExpressionPropertyDialog(ParameterTypeExpression type, Process process, String initialValue) { super(type, "expression"); // add observers to receive model updates inputsModel.addObserver(inputObserver, false); functionModel.addObserver(functionObserver, false); // create ExpressionParser with Process to enable Process functions controllingProcess = getControllingProcessOrNull(type, process); // use the ExpressionParserBuilder to create the parser ExpressionParserBuilder builder = new ExpressionParserBuilder(); // use a compatibility level to only show functions that are available if (type.getInputPort() != null) { builder = builder.withCompatibility(type.getInputPort().getPorts().getOwner().getOperator() .getCompatibilityLevel()); } if (controllingProcess != null) { builder = builder.withProcess(controllingProcess); // make macros available to the parser builder = builder.withScope(new MacroResolver(controllingProcess.getMacroHandler())); } // make attributes available to the parser InputPort inPort = ((ParameterTypeExpression) getParameterType()).getInputPort(); if (inPort != null) { if (inPort.getMetaData() instanceof ExampleSetMetaData) { ExampleSetMetaData emd = (ExampleSetMetaData) inPort.getMetaData(); if (emd != null) { builder = builder.withDynamics(new ExampleResolver(emd)); } } else if (inPort.getMetaData() instanceof ModelMetaData) { ModelMetaData mmd = (ModelMetaData) inPort.getMetaData(); if (mmd != null) { ExampleSetMetaData emd = mmd.getTrainingSetMetaData(); if (emd != null) { builder = builder.withDynamics(new ExampleResolver(emd)); } } } } // show all registered modules builder = builder.withModules(ExpressionRegistry.INSTANCE.getAll()); // finally create the parser parser = builder.build(); // fetch the expression context from the parser // add the current function inputs to the functions input model inputsModel.addContent(parser.getExpressionContext().getFunctionInputs()); // add the existing functions to the functions model functionModel.addContent(parser.getExpressionContext().getFunctionDescriptions()); // remove deprecated expression context from the syntax highlighting ExpressionTokenMaker.removeFunctionInputs(); // add the current expression context to the syntax highlighting ExpressionTokenMaker.addFunctionInputs(parser.getExpressionContext().getFunctionInputs()); ExpressionTokenMaker.addFunctions(parser.getExpressionContext().getFunctionDescriptions()); // initialize the UI initGui(initialValue); FunctionDescriptionPanel.updateMaximalWidth(functionsPanel.getSize().width); updateFunctions(); } private Process getControllingProcessOrNull(ParameterTypeExpression type, Process process) { if (process != null) { return process; } else if (type.getInputPort() != null) { return type.getInputPort().getPorts().getOwner().getOperator().getProcess(); } else { return null; } } /** * Initializes the UI of the dialog. * * @param initialValue * of the expression */ public void initGui(String initialValue) { // this is the only way to set colors for the JXTaskPane component /* background color */ UIManager.put("TaskPane.background", LIGHTER_GRAY); /* title hover color */ UIManager.put("TaskPane.titleOver", SwingTools.RAPIDMINER_ORANGE); UIManager.put("TaskPane.specialTitleOver", SwingTools.RAPIDMINER_ORANGE); /* border color */ UIManager.put("TaskPane.borderColor", LIGHTER_GRAY); /* foreground */ UIManager.put("TaskPane.foreground", Color.black); UIManager.put("TaskPane.titleForeground", Color.black); UIManager.put("TaskPane.specialTitleForeground", Color.black); /* title background */ UIManager.put("TaskPane.specialTitleBackground", COLOR_TITLE_TASKPANE_BACKGROUND); UIManager.put("TaskPane.titleBackgroundGradientStart", COLOR_TITLE_TASKPANE_BACKGROUND); UIManager.put("TaskPane.titleBackgroundGradientEnd", COLOR_TITLE_TASKPANE_BACKGROUND); // add OK and cancel button Collection<AbstractButton> buttons = new LinkedList<AbstractButton>(); final JButton okButton = makeOkButton("expression_property_dialog_apply"); buttons.add(okButton); buttons.add(makeCancelButton()); // Create the main panel 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.gridwidth = 2; mainC.gridx = 0; mainC.gridy = 0; mainC.insets = new Insets(0, STD_INSET_GBC, 0, STD_INSET_GBC); // EXPRESSION JPanel expressionPanel = new JPanel(); GridBagLayout expressionLayout = new GridBagLayout(); expressionPanel.setLayout(expressionLayout); GridBagConstraints expressionC = new GridBagConstraints(); expressionC.fill = GridBagConstraints.BOTH; expressionC.insets = new Insets(STD_INSET_GBC, STD_INSET_GBC, STD_INSET_GBC, STD_INSET_GBC); expressionC.gridx = 0; expressionC.gridy = 0; expressionC.gridwidth = 2; expressionC.weightx = 0; expressionC.weighty = 0; // expression title JLabel label = new JLabel("<html><b><font size=" + FONT_SIZE_HEADER + ">Expression</font></b></html>"); expressionC.gridy += 1; expressionPanel.add(label, expressionC); // current expression // validate the expression when it was changed currentExpression.addKeyListener(new KeyListener() { @Override public void keyTyped(KeyEvent e) {} @Override public void keyPressed(KeyEvent e) { if (e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK) { if (e.getExtendedKeyCode() == KeyEvent.VK_ENTER) { okButton.doClick(); } } } @Override public void keyReleased(KeyEvent e) { validateExpression(); } }); // Use the custom token maker to highlight RapidMiner expressions AbstractTokenMakerFactory atmf = (AbstractTokenMakerFactory) TokenMakerFactory.getDefaultInstance(); atmf.putMapping("text/expression", "com.rapidminer.gui.tools.syntax.ExpressionTokenMaker"); currentExpression.setSyntaxEditingStyle("text/expression"); // the current line should not be highlighted currentExpression.setHighlightCurrentLine(false); // enable bracket matching (just works if brackets have the token type Token.SEPARATOR) currentExpression.setBracketMatchingEnabled(true); currentExpression.setAnimateBracketMatching(true); currentExpression.setPaintMatchedBracketPair(true); // set initial size currentExpression.setRows(NUMBER_OF_EXPRESSION_ROWS); // set custom colors for syntax highlighting currentExpression.setSyntaxScheme(getExpressionColorScheme(currentExpression.getSyntaxScheme())); currentExpression.setBorder(BorderFactory.createEmptyBorder()); scrollPaneExpression = new RTextScrollPane(currentExpression, true); scrollPaneExpression.setMinimumSize(new Dimension(getMinimumSize().width, HEIGHT_EXPRESSION_SCROLL_PANE)); scrollPaneExpression.setPreferredSize(new Dimension(getPreferredSize().width, HEIGHT_EXPRESSION_SCROLL_PANE)); scrollPaneExpression.setMaximumSize(new Dimension(getMaximumSize().width, HEIGHT_EXPRESSION_SCROLL_PANE)); scrollPaneExpression.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 0, COLOR_BORDER_EXPRESSION)); scrollPaneExpression.getVerticalScrollBar().setBorder( BorderFactory.createMatteBorder(0, 0, 0, 1, Colors.TEXTFIELD_BORDER)); // use the gutter to display an error icon in the line with an error Gutter gutter = scrollPaneExpression.getGutter(); gutter.setBookmarkingEnabled(true); expressionC.gridy += 1; expressionC.weightx = 1; expressionC.insets = new Insets(0, STD_INSET_GBC, STD_INSET_GBC, STD_INSET_GBC); expressionC.fill = GridBagConstraints.BOTH; expressionC.gridwidth = 6; expressionPanel.add(scrollPaneExpression, expressionC); JPanel validationPanel = new JPanel(); validationPanel.setLayout(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 1; gbc.anchor = GridBagConstraints.NORTHWEST; gbc.fill = GridBagConstraints.BOTH; // insert validation label validationLabel.setAlignmentX(SwingConstants.LEFT); validationPanel.add(validationLabel, gbc); gbc.gridy += 1; validationTextArea.setAlignmentX(SwingConstants.LEFT); validationTextArea.setEditable(false); validationTextArea.setRows(2); validationTextArea.setOpaque(false); validationTextArea.setBorder(BorderFactory.createEmptyBorder()); validationPanel.add(validationTextArea, gbc); expressionC.fill = GridBagConstraints.BOTH; expressionC.insets = new Insets(STD_INSET_GBC, STD_INSET_GBC, 0, STD_INSET_GBC); expressionC.gridx = 0; expressionC.weightx = 1; expressionC.gridy += 1; expressionC.gridwidth = 6; expressionC.anchor = GridBagConstraints.NORTHWEST; expressionPanel.add(validationPanel, expressionC); // add expression part to the main panel mainPanel.add(expressionPanel, mainC); // FUNCTIONS JPanel functionPanel = new JPanel(); GridBagLayout functionsLayout = new GridBagLayout(); functionPanel.setLayout(functionsLayout); GridBagConstraints functionsC = new GridBagConstraints(); // add functions title functionsC.insets = new Insets(0, STD_INSET_GBC, STD_INSET_GBC, STD_INSET_GBC); functionsC.gridy = 0; functionsC.gridx = 0; functionsC.anchor = GridBagConstraints.NORTHWEST; functionPanel .add(new JLabel("<html><b><font size=" + FONT_SIZE_HEADER + ">Functions</font></b></html>"), functionsC); functionsC.insets = new Insets(0, 0, STD_INSET_GBC, STD_INSET_GBC); functionsC.gridx += 1; functionsC.anchor = GridBagConstraints.SOUTHEAST; // add search field for functions functionsFilterField.addFilterListener(filterFunctionsListener); TextFieldWithAction textField = new TextFieldWithAction(functionsFilterField, clearFunctionsFilterAction, CLEAR_FILTER_HOVERED_ICON); textField.setMinimumSize(new Dimension(WIDTH_FUNCTION_SEARCH_FIELD, HEIGHT_SEARCH_FIELD)); textField.setPreferredSize(new Dimension(WIDTH_FUNCTION_SEARCH_FIELD, HEIGHT_SEARCH_FIELD)); textField.setMaximumSize(new Dimension(WIDTH_FUNCTION_SEARCH_FIELD, HEIGHT_SEARCH_FIELD)); functionPanel.add(textField, functionsC); // create the functions panel and display the existing functions updateFunctions(); JPanel outerFunctionPanel = new JPanel(); outerFunctionPanel.setLayout(new GridBagLayout()); GridBagConstraints outerFunctionC = new GridBagConstraints(); outerFunctionC.gridwidth = GridBagConstraints.REMAINDER; outerFunctionC.fill = GridBagConstraints.HORIZONTAL; outerFunctionC.weightx = 1; outerFunctionC.weighty = 1; outerFunctionC.anchor = GridBagConstraints.NORTHWEST; functionsPanel.setBackground(LIGHTER_GRAY); outerFunctionPanel.add(functionsPanel, outerFunctionC); outerFunctionC.fill = GridBagConstraints.BOTH; JPanel gapPanel2 = new JPanel(); gapPanel2.setBackground(LIGHTER_GRAY); outerFunctionPanel.add(gapPanel2, outerFunctionC); outerFunctionPanel.setBackground(LIGHTER_GRAY); // add the functions panel to the scroll bar functionButtonScrollPane = new ExtendedJScrollPane(outerFunctionPanel); functionButtonScrollPane.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, Colors.TEXTFIELD_BORDER)); // the scroll bar should always be visible functionButtonScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); // add scroll pane to function panel functionsC.gridx = 0; functionsC.gridy += 1; functionsC.fill = GridBagConstraints.BOTH; functionsC.weightx = 1; functionsC.weighty = 1; functionsC.insets = new Insets(0, STD_INSET_GBC, STD_INSET_GBC, STD_INSET_GBC); functionsC.gridwidth = 2; functionsC.anchor = GridBagConstraints.NORTH; functionPanel.add(functionButtonScrollPane, functionsC); // add function panel to the main panel mainC.weighty = 1; mainC.gridwidth = 1; mainC.weightx = 0.9; mainC.gridy += 1; mainPanel.add(functionPanel, mainC); // INPUTS inputsLayout = new GridBagLayout(); inputsPanel.setLayout(inputsLayout); // add inputs panel to the outer panel JPanel outerInputPanel = new JPanel(); outerInputPanel.setLayout(new GridBagLayout()); GridBagConstraints outerInputC = new GridBagConstraints(); outerInputC.gridwidth = GridBagConstraints.REMAINDER; outerInputC.fill = GridBagConstraints.HORIZONTAL; outerInputC.weightx = 1; outerInputC.weighty = 1; outerInputC.anchor = GridBagConstraints.NORTHWEST; inputsPanel.setBackground(LIGHTER_GRAY); outerInputPanel.add(inputsPanel, outerInputC); outerInputC.weighty = 1; outerInputC.fill = GridBagConstraints.BOTH; JPanel gapPanel = new JPanel(); gapPanel.setBackground(LIGHTER_GRAY); outerInputPanel.add(gapPanel, outerInputC); // and update the view of the inputs if (inputsModel.getFilteredModel().size() > 0) { updateInputs(); // add inputs title JPanel outerInputsPanel = new JPanel(); outerInputsPanel.setLayout(new GridBagLayout()); outerInputsPanel.setMinimumSize(new Dimension(WIDTH_ATTRIBUTE_PANEL, getMinimumSize().height)); outerInputsPanel.setPreferredSize(new Dimension(WIDTH_ATTRIBUTE_PANEL, getPreferredSize().height)); outerInputsPanel.setMaximumSize(new Dimension(WIDTH_ATTRIBUTE_PANEL, getMaximumSize().height)); GridBagConstraints outerGBC = new GridBagConstraints(); outerGBC.gridx = 0; outerGBC.gridy = 0; outerGBC.insets = new Insets(0, STD_INSET_GBC, STD_INSET_GBC, STD_INSET_GBC); outerGBC.anchor = GridBagConstraints.NORTHWEST; outerInputsPanel.add(new JLabel("<html><b><font size=" + FONT_SIZE_HEADER + ">Inputs</font></b></html>"), outerGBC); outerGBC.gridx += 1; outerGBC.weightx = 1; outerInputsPanel.add(new JLabel(" "), outerGBC); // add search text field for FunctionInputs outerGBC.gridx += 1; outerGBC.weightx = 0.1; outerGBC.anchor = GridBagConstraints.SOUTHEAST; outerGBC.insets = new Insets(0, 0, STD_INSET_GBC, 0); inputsFilterField.addFilterListener(filterInputsListener); TextFieldWithAction inputTextField = new TextFieldWithAction(inputsFilterField, clearInputsFilterAction, CLEAR_FILTER_HOVERED_ICON); inputTextField.setMaximumSize(new Dimension(WIDTH_INPUTS_SEARCH_FIELD, HEIGHT_SEARCH_FIELD)); inputTextField.setPreferredSize(new Dimension(WIDTH_INPUTS_SEARCH_FIELD, HEIGHT_SEARCH_FIELD)); inputTextField.setMinimumSize(new Dimension(WIDTH_INPUTS_SEARCH_FIELD, HEIGHT_SEARCH_FIELD)); outerInputsPanel.add(inputTextField, outerGBC); // Add type filter for nominal input values chbNominal = new JCheckBox(new ResourceAction(true, "expression_property_dialog.quick_filter.nominal") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent arg0) { inputsModel.setNominalFilter(chbNominal.isSelected()); } }); chbNominal.setSelected(inputsModel.isNominalFilterToggled()); // Add type filter for numerical input values chbNumeric = new JCheckBox(new ResourceAction(true, "expression_property_dialog.quick_filter.numerical") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent arg0) { inputsModel.setNumericFilter(chbNumeric.isSelected()); } }); chbNumeric.setSelected(inputsModel.isNumericFilterToggled()); // Add type filter for date time input values chbDateTime = new JCheckBox(new ResourceAction(true, "expression_property_dialog.quick_filter.date_time") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent arg0) { inputsModel.setDateTimeFilter(chbDateTime.isSelected()); } }); chbDateTime.setSelected(inputsModel.isDateTimeFilterToggled()); // create the menu with the type filters final ScrollableJPopupMenu filterMenu = new ScrollableJPopupMenu(); // small hack to prevent the popup from opening itself when you click the button to // actually // close it filterMenu.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(final PopupMenuEvent e) {} @Override public void popupMenuWillBecomeInvisible(final PopupMenuEvent e) { lastPopupCloseTime = System.currentTimeMillis(); } @Override public void popupMenuCanceled(final PopupMenuEvent e) {} }); filterMenu.add(chbNominal); filterMenu.add(chbNumeric); filterMenu.add(chbDateTime); // create button to open the type filter menu final JButton filterDropdownButton = new JButton(ICON_FILTER); filterDropdownButton.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); filterDropdownButton.setContentAreaFilled(false); filterDropdownButton.addMouseListener(new HoverBorderMouseListener(filterDropdownButton)); filterDropdownButton.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.label.expression_property_dialog.quick_filter.filter_select.tip")); // show the menu when the button is clicked filterDropdownButton.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { if (!filterMenu.isVisible()) { // hack to prevent filter popup from opening itself again when you click the // button to actually close it while it is open if (System.currentTimeMillis() - lastPopupCloseTime < 250) { return; } int menuWidth = filterMenu.getSize().width; if (menuWidth == 0) { // guess the correct width for the first opening menuWidth = 108; } filterMenu.show(filterDropdownButton, -menuWidth + filterDropdownButton.getSize().width, filterDropdownButton.getHeight()); filterMenu.requestFocusInWindow(); } } }); outerGBC.gridx += 1; outerGBC.insets = new Insets(0, 0, STD_INSET_GBC, STD_INSET_GBC); outerInputsPanel.add(filterDropdownButton, outerGBC); // create scroll bar for inputs outerInputPanel.setBackground(LIGHTER_GRAY); ExtendedJScrollPane inputsScrollPane = new ExtendedJScrollPane(outerInputPanel); inputsScrollPane.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, Colors.TEXTFIELD_BORDER)); inputsScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); outerGBC.gridx = 0; outerGBC.insets = new Insets(0, STD_INSET_GBC, STD_INSET_GBC, STD_INSET_GBC); outerGBC.gridwidth = 4; outerGBC.gridy += 1; outerGBC.fill = GridBagConstraints.BOTH; outerGBC.anchor = GridBagConstraints.NORTH; outerGBC.weightx = 1; outerGBC.weighty = 1; outerInputsPanel.add(inputsScrollPane, outerGBC); // add inputs part to the main panel mainC.weightx = 0.1; mainC.gridx += 1; mainPanel.add(outerInputsPanel, mainC); } setIconImage(SwingTools.createIcon("16/rapidminer_studio.png").getImage()); layoutDefault(mainPanel, HUGE, buttons.toArray(new AbstractButton[buttons.size()])); // if an initial value is given, set it if (initialValue != null) { currentExpression.setText(initialValue); } // validate the expression validateExpression(); setResizable(false); } /** * @return current expression */ public String getExpression() { return currentExpression.getText(); } @Override protected Icon getInfoIcon() { return null; } @Override protected String getInfoText() { return ""; } /** * Adds the given value to the expression text * * @param value * that is added to the expression text */ 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(); } } /** * Adds the given value (function name) to the expression text. If the function has no * arguments, the caret is placed after the function's right parenthesis. * * @param value * @param function */ private void addToExpression(String value, FunctionDescription function) { addToExpression(value); if (function.getNumberOfArguments() == 0) { currentExpression.setCaretPosition(currentExpression.getCaretPosition() + 1); } } /** * Requests focus on the expression text */ private void requestExpressionFocus() { currentExpression.requestFocusInWindow(); } /** * Updates the function panel */ private void updateFunctions() { // remove all content functionsPanel.removeAll(); functionsPanel.setLayout(functionButtonsLayout); functionCategoryTaskPanes = new HashMap<>(); GridBagConstraints gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1; gbc.gridx = 0; gbc.gridy = 0; int totalFunctionCount = 0; // get the filtered model (the FunctionDescriptions we want to display) Map<String, List<FunctionDescription>> filteredModel = functionModel.getFilteredModel(); String filterName = functionModel.getFilterNameString(); boolean searchStringGiven = !filterName.isEmpty(); for (String functionGroup : filteredModel.keySet()) { boolean perfectMatch = false; JXTaskPane functionCategoryTaskPane = new JXTaskPane(); functionCategoryTaskPane.setName(functionGroup); List<FunctionDescription> list = functionModel.getFilteredModel(functionGroup); totalFunctionCount += list.size(); for (FunctionDescription function : list) { // create the panels for the functions, register the observer and add them to the // related category final FunctionDescriptionPanel funcDescPanel = new FunctionDescriptionPanel(function); funcDescPanel.registerObserver(functionObserver); functionCategoryTaskPane.add(funcDescPanel); if (!perfectMatch && searchStringGiven) { // check for function name equality without brackets and with brackets String functionName = function.getDisplayName().split("\\(")[0]; if (filterName.toLowerCase(Locale.ENGLISH).equals(functionName.toLowerCase(Locale.ENGLISH)) || filterName.toLowerCase(Locale.ENGLISH).equals( function.getDisplayName().toLowerCase(Locale.ENGLISH))) { perfectMatch = true; } } } functionCategoryTaskPane.setTitle(functionGroup); functionCategoryTaskPane.setAnimated(false); // if there is only one category in the filtered model, open the task pane if (filteredModel.keySet().size() == 1) { functionCategoryTaskPane.setCollapsed(false); } else { functionCategoryTaskPane.setCollapsed(true); } if (perfectMatch) { functionCategoryTaskPane.setCollapsed(false); } functionCategoryTaskPanes.put(functionGroup, functionCategoryTaskPane); gbc.ipady = 10; functionsPanel.add(functionCategoryTaskPane, gbc); gbc.gridy += 1; } // if the number of result functions is clear, open the task panes // (if you can see all categories even if they are opened) if (totalFunctionCount <= MAX_NMBR_FUNCTIONS_SHOWN) { for (JXTaskPane taskPane : functionCategoryTaskPanes.values()) { taskPane.setCollapsed(false); } } // if there are no results, show a simple message if (filteredModel.isEmpty()) { gbc.ipady = 10; functionsPanel.add(new JLabel(MESSAGE_NO_RESULTS), gbc); } functionsPanel.revalidate(); } /** * updates the inputs panel */ private void updateInputs() { // remove all content inputsPanel.removeAll(); inputsPanel.setLayout(inputsLayout); inputCategoryTaskPanes = new HashMap<>(); GridBagConstraints gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1; gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST; int totalEntryCount = 0; // get the filtered model (the FunctionInputs we want to display) Map<String, List<FunctionInput>> filteredModel = inputsModel.getFilteredModel(); String filterName = inputsModel.getFilterNameString(); boolean searchStringGiven = !filterName.isEmpty(); List<String> keySet = new LinkedList<>(filteredModel.keySet()); boolean anyPerfectMatch = false; for (String type : keySet) { boolean perfectMatch = false; JXTaskPane inputCategoryTaskPane = new JXTaskPane(); inputCategoryTaskPane.setName(type); List<FunctionInput> list = inputsModel.getFilteredModel(type); for (FunctionInput entry : list) { // set a value where we want to see a role, value or description in a second line String value = null; if (entry.getCategory() == Category.DYNAMIC || entry.getCategory() == Category.CONSTANT) { totalEntryCount += 1; value = entry.getAdditionalInformation(); } else if (entry.getCategory() == Category.SCOPE) { totalEntryCount += 1; // use the current scope value value = parser.getExpressionContext().getScopeString(entry.getName()); } FunctionInputPanel inputPanel = null; // create the panels for the inputs, register the observer and add them to the // related category if (value == null) { inputPanel = new FunctionInputPanel(entry); } else { inputPanel = new FunctionInputPanel(entry, value); } inputPanel.registerObserver(inputObserver); inputCategoryTaskPane.add(inputPanel); if (!perfectMatch && searchStringGiven) { // check if the input name is equal to search term if (filterName.toLowerCase(Locale.ENGLISH).equals(entry.getName().toLowerCase(Locale.ENGLISH))) { perfectMatch = true; anyPerfectMatch = true; } } } inputCategoryTaskPane.setTitle(type); inputCategoryTaskPane.setAnimated(false); // if there is only one category in the filtered model, open the task pane if (filteredModel.keySet().size() == 1) { inputCategoryTaskPane.setCollapsed(false); } else { inputCategoryTaskPane.setCollapsed(true); } if (perfectMatch) { inputCategoryTaskPane.setCollapsed(false); } inputCategoryTaskPanes.put(type, inputCategoryTaskPane); gbc.ipady = 10; inputsPanel.add(inputCategoryTaskPane, gbc); gbc.gridy += 1; } // if the number of result inputs is clear, open the task panes // (if you can see all categories even if they are opened) if (totalEntryCount <= MAX_NMBR_INPUTS_SHOWN) { for (JXTaskPane taskPane : inputCategoryTaskPanes.values()) { taskPane.setCollapsed(false); } } else { // if there was no perfect match open attributes if there are not too much entries if (!anyPerfectMatch) { // if attributes can be opened such that you can see that there are more categories, // open the attributes categories if (filteredModel.get(ExampleResolver.KEY_ATTRIBUTES) != null && filteredModel.get(ExampleResolver.KEY_SPECIAL_ATTRIBUTES) != null && filteredModel.get(ExampleResolver.KEY_ATTRIBUTES).size() + filteredModel.get(ExampleResolver.KEY_SPECIAL_ATTRIBUTES).size() <= MAX_NMBR_INPUTS_SHOWN) { inputCategoryTaskPanes.get(ExampleResolver.KEY_ATTRIBUTES).setCollapsed(false); inputCategoryTaskPanes.get(ExampleResolver.KEY_SPECIAL_ATTRIBUTES).setCollapsed(false); } else if (filteredModel.get(ExampleResolver.KEY_ATTRIBUTES) != null && filteredModel.get(ExampleResolver.KEY_ATTRIBUTES).size() <= MAX_NMBR_INPUTS_SHOWN) { inputCategoryTaskPanes.get(ExampleResolver.KEY_ATTRIBUTES).setCollapsed(false); } } } // if there are no results, show a simple message if (filteredModel.isEmpty()) { gbc.ipady = 10; inputsPanel.add(new JLabel(MESSAGE_NO_RESULTS), gbc); } inputsPanel.revalidate(); } /** * Validates the syntax of the current text in the expression field */ private void validateExpression() { // remove error buttons removeLineSignals(); String expression = currentExpression.getText(); if (expression != null) { if (expression.trim().length() > 0) { try { // make a syntax check parser.checkSyntax(expression); // show status of the expression showError(false, "<b>Info: </b>", "Expression is syntactically correct."); } catch (ExpressionException e) { // show status of the expression showError(true, "<b>Error: </b>", e.getMessage()); // if the line of the error is given, show an error icon in this line int line = e.getErrorLine(); if (line > 0) { signalLine(line); } return; } } else { // show status of the expression showError(false, "", "Please specify a valid expression."); } } else { // show status of the expression showError(false, "", "Please specify a valid expression."); } } /** * Changes the {@link SyntaxScheme} of a {@link RSyntaxTextArea} to use custom colors * * @param textAreaSyntaxScheme * the {@link SyntaxScheme} which should be changed * @return the changed {@link SyntaxScheme} with custom colors */ private SyntaxScheme getExpressionColorScheme(SyntaxScheme textAreaSyntaxScheme) { SyntaxScheme ss = textAreaSyntaxScheme; // show brackets in dark purple ss.setStyle(Token.SEPARATOR, new Style(DARK_PURPLE)); // show double quotes / strings in dark cyan ss.setStyle(Token.LITERAL_STRING_DOUBLE_QUOTE, new Style(DARK_CYAN)); // show attributes in RapidMiner orange ss.setStyle(Token.VARIABLE, new Style(SwingTools.RAPIDMINER_ORANGE)); // show unknown attributes that are placed in brackets in [] in black ss.setStyle(Token.COMMENT_KEYWORD, new Style(Color.black)); // show operators that are not defined in the functions in black (like other unknown words) ss.setStyle(Token.OPERATOR, new Style(Color.black)); return ss; } /** * Removes all line signals that show error occurrences */ private void removeLineSignals() { scrollPaneExpression.getGutter().removeAllTrackingIcons(); } /** * Shows an error icon in the given line * * @param line * that should show an error icon */ private void signalLine(int line) { // use the gutter of the expression scroll pane to display icons try { scrollPaneExpression.getGutter().addLineTrackingIcon(line - 1, ERROR_ICON); } catch (BadLocationException e) { // in this case don't show an error icon } } /** * Show a title and a message (error or information) about the status of the expression * * @param error * if the message is an error message * @param title * title of the message * @param message * message, which shows in case of error the place of the error */ private void showError(boolean error, String title, String message) { // set colors accordingly if (error) { validationLabel.setForeground(Color.RED); validationTextArea.setForeground(Color.RED); } else { validationLabel.setForeground(DARK_GREEN); validationTextArea.setForeground(DARK_GREEN); } // add the explanation line to the label to use a different font and the same indentation as // the title String[] splittedMessage = message.split("\n"); String explanation = splittedMessage.length > 0 ? splittedMessage[0] : ""; explanation = (explanation.charAt(0) + "").toUpperCase() + explanation.substring(1); validationLabel.setText("<html>" + title + explanation + "</html>"); // show the error message with the place of the error in monospaced // DO NOT CHANGE THIS, AS THE INDENTATION IS WRONG OTHERWISE validationTextArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); // set the error message // strip the error message if necessary if (splittedMessage.length > 1) { int buffer = 50; int stepsize = 5; int stringWidth = SwingTools.getStringWidth(validationTextArea, splittedMessage[1]) + buffer; boolean cut = false; while (stringWidth > validationTextArea.getSize().width - stepsize) { cut = true; splittedMessage[1] = splittedMessage[1].substring(stepsize, splittedMessage[1].length()); if (splittedMessage.length > 2) { splittedMessage[2] = splittedMessage[2].substring(stepsize, splittedMessage[2].length()); } stringWidth = SwingTools.getStringWidth(validationTextArea, splittedMessage[1]) + buffer; } if (cut) { splittedMessage[1] = "[...]" + splittedMessage[1]; if (splittedMessage.length > 2) { splittedMessage[2] = " " + splittedMessage[2]; } } } String errorMessage = splittedMessage.length > 1 ? splittedMessage[1] : "\n"; if (splittedMessage.length > 2) { errorMessage += "\n" + splittedMessage[2]; } validationTextArea.setText(errorMessage); } }