/**
* 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.tablepanel;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.SwingUtilities;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import com.rapidminer.gui.properties.tablepanel.cells.implementations.CellTypeCheckBoxImpl;
import com.rapidminer.gui.properties.tablepanel.cells.implementations.CellTypeComboBoxImpl;
import com.rapidminer.gui.properties.tablepanel.cells.implementations.CellTypeDateImpl;
import com.rapidminer.gui.properties.tablepanel.cells.implementations.CellTypeRegexImpl;
import com.rapidminer.gui.properties.tablepanel.cells.implementations.CellTypeTextFieldDefaultImpl;
import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellType;
import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeCheckBox;
import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeComboBox;
import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeDate;
import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeDateTime;
import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeRegex;
import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeTextFieldDefault;
import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeTextFieldInteger;
import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeTextFieldNumerical;
import com.rapidminer.gui.properties.tablepanel.cells.interfaces.CellTypeTextFieldTime;
import com.rapidminer.gui.properties.tablepanel.model.TablePanelModel;
import com.rapidminer.gui.tools.ExtendedJScrollPane;
import com.rapidminer.gui.tools.ResourceAction;
import com.rapidminer.tools.container.Pair;
/**
* This component can display a {@link TablePanelModel} in a table-like structure without actually
* using a table. It also supports content assist (if available) for textfields, a date picker for
* date fields and a regular expression dialog for regex fields. <br/>
* It displays contents of the model via GUI components depending on the
* {@link TablePanelModel#getColumnClass(int, int)}.
* <p/>
* GUI mapping:
* <ul>
* <li>CellTypeComboBox.class - {@link CellTypeComboBoxImpl} with auto complete if it is enabled</li>
* <li>CellTypeTextFieldDefault.class - {@link CellTypeTextFieldDefaultImpl} with possible content
* assist</li>
* <li>CellTypeTextFieldNumerical.class - {@link CellTypeTextFieldDefaultImpl} which only accepts
* numbers with possible content assist</li>
* <li>CellTypeTextFieldInteger.class - {@link CellTypeTextFieldDefaultImpl} which only accepts
* integers with possible content assist</li>
* <li>CellTypeTextFieldDate.class - {@link CellTypeDateImpl} with date picker</li>
* <li>CellTypeTextFieldTime.class - {@link CellTypeTextFieldDefaultImpl} with possible content
* assist</li>
* <li>CellTypeTextFieldDateTime.class - {@link CellTypeDateImpl} with date picker</li>
* <li>CellTypeTextFieldExpression.class - {@link CellTypeRegexImpl} with regular expression assist
* dialog</li>
* <li>CellTypeCheckBox.class - {@link CellTypeCheckBoxImpl}</li>
* <li>fallback - {@link JLabel}</li>
* </ul>
* These elements might be disabled or enabled depending on
* {@link TablePanelModel#isCellEditable(int, int)}.
* <p/>
* If content assist is available, will display {@link JCheckBox}es for multiple available values
* and {@link JRadioButton}s when only one value is supported.
* <p/>
* For more references regarding the support for content assist see
* {@link TablePanelModel#isContentAssistPossibleForCell(int, int)},<br/>
* {@link TablePanelModel#getPossibleValuesForCellOrNull(int, int)} and<br/>
* {@link TablePanelModel#canCellHaveMultipleValues(int, int)}.
*
* @author Marco Boeck
*
*/
public class TablePanel extends JPanel {
/**
* Defines how additional space is distributed when using fixed width mode.
*
*
* @author Marco Boeck
*
*/
public static enum FillerMode {
/**
* additional space is not filled in any way, components will be centered in the middle of
* their respective location
*/
NONE,
/** additional space is filled between the last component per row and the delete row button */
IN_BETWEEN,
/** additional space is filled after the delete row button */
REMAINDER;
}
private static final long serialVersionUID = 7783828436200806566L;
/** the backing model */
private TablePanelModel model;
/** the listener for the table model */
private TableModelListener listener;
/** flag indicating if this component should take care of the scrollpane around it */
private boolean useScrollPane;
/** if <code>true</code>, hides the content assist button when not ca is available */
private boolean hideUnavailableContentAssist;
/** the size constraints for each column */
private Dimension[] constraints;
/** the mode how additional space should be filled */
private FillerMode fillerMode = FillerMode.IN_BETWEEN;
/** holds the inner panel */
private ExtendedJScrollPane scrollPane;
/** holds all dynamically created components */
private JPanel innerPanel;
/** global GridBagConstraints for the dynamically created components */
private GridBagConstraints gbc;
/** stores all component (aka cells) to allow for easy replacement */
private Map<Pair<Integer, Integer>, Component> mapOfComponents;
/**
* Creates a new {@link TablePanel} instance.
*
* @param model
* @param useScrollPane
* if set to <code>true</code>, will add a scrollpane around the GUI.
* @param hideUnavailableContentAssist
* if <code>true</code>, the content assist button will be hidden if no content
* assist is available for the given field
*/
public TablePanel(final TablePanelModel model, boolean useScrollPane, boolean hideUnavailableContentAssist) {
this.mapOfComponents = new HashMap<>();
this.useScrollPane = useScrollPane;
this.hideUnavailableContentAssist = hideUnavailableContentAssist;
this.listener = new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
// table structure changed, re-create it
if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
createGUI();
} else {
updateComponent(e.getFirstRow(), e.getColumn());
}
}
};
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
initGUI();
setModel(model);
}
});
}
/**
* Inits the GUI.
*/
private void initGUI() {
scrollPane = new ExtendedJScrollPane();
scrollPane.setBorder(null);
innerPanel = new JPanel();
}
/**
* Creates the GUI via the table model data.
*/
private void createGUI() {
mapOfComponents.clear();
setLayout(new BorderLayout());
removeAll();
innerPanel.removeAll();
innerPanel.setLayout(new GridBagLayout());
if (useScrollPane) {
add(scrollPane, BorderLayout.CENTER);
} else {
add(innerPanel);
}
gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.anchor = GridBagConstraints.PAGE_START;
// iterate over table-like structure and display it
for (int rowIndex = 0; rowIndex < model.getRowCount(); rowIndex++) {
if (isConstraintsUsed() && fillerMode == FillerMode.NONE) {
gbc.fill = GridBagConstraints.VERTICAL;
} else {
gbc.fill = GridBagConstraints.BOTH;
}
for (int columnIndex = 0; columnIndex < model.getColumnCount(); columnIndex++) {
if (isConstraintsUsed()) {
gbc.weightx = 0.0;
} else {
if (Collection.class.isAssignableFrom(model.getColumnClass(columnIndex))) {
gbc.weightx = 0.1;
} else {
gbc.weightx = 1.0 / model.getColumnCount();
}
}
gbc.gridx = columnIndex;
gbc.gridy = rowIndex;
Component component = createComponentForCell(rowIndex, columnIndex);
// add dimension constraint (if applicable)
if (isConstraintsUsed()) {
component.setMinimumSize(constraints[columnIndex]);
component.setMaximumSize(constraints[columnIndex]);
component.setPreferredSize(constraints[columnIndex]);
}
mapOfComponents.put(new Pair<>(rowIndex, columnIndex), component);
innerPanel.add(component, gbc);
}
// if filler mode is IN_BETWEEN, add filler component here
if (isConstraintsUsed() && fillerMode == FillerMode.IN_BETWEEN) {
gbc.weightx = 1.0;
gbc.fill = GridBagConstraints.BOTH;
gbc.gridx += 1;
innerPanel.add(new JLabel(), gbc);
}
// add "remove row" button
gbc.weightx = 0.0;
gbc.fill = GridBagConstraints.NONE;
gbc.gridx = model.getColumnCount();
gbc.anchor = GridBagConstraints.EAST;
final int row = rowIndex;
JButton removeRowButton = new JButton(new ResourceAction(true, "list.remove_entry") {
private static final long serialVersionUID = 5289974084350157673L;
@Override
public void actionPerformed(ActionEvent e) {
model.removeRow(row);
}
});
removeRowButton.setText(null);
removeRowButton.setPreferredSize(new Dimension(44, 33));
removeRowButton.setContentAreaFilled(false);
removeRowButton.setBorderPainted(false);
innerPanel.add(removeRowButton, gbc);
// if filler mode is REMAINDER, add filler component here
if (isConstraintsUsed() && fillerMode == FillerMode.REMAINDER) {
gbc.weightx = 1.0;
gbc.fill = GridBagConstraints.BOTH;
gbc.gridx += 1;
innerPanel.add(new JLabel(), gbc);
}
}
// filler component so the others are placed neatly at the top
gbc.gridy++;
gbc.weighty = 1.0;
gbc.fill = GridBagConstraints.VERTICAL;
innerPanel.add(Box.createVerticalBox(), gbc);
if (useScrollPane) {
scrollPane.setViewportView(innerPanel);
}
validate();
repaint();
}
/**
* Set the backing model.
*
* @param model
*/
public void setModel(TablePanelModel model) {
// unregister ourself if we had a model before
if (this.model != null) {
this.model.removeTableModelListener(listener);
}
this.model = model;
this.model.addTableModelListener(listener);
// model changed, re-create GUI
createGUI();
}
/**
* Returns the {@link TablePanelModel} instance of this {@link TablePanel}.
*
* @return
*/
public TablePanelModel getModel() {
return this.model;
}
/**
* Defines how additional space is distributed in each row if dimension constraints have been
* set via {@link #setDimensionConstraints(Dimension[])}. See {@link FillerMode} for a
* description of each mode.
*
* @param fillerMode
*/
public void setFillerMode(FillerMode fillerMode) {
this.fillerMode = fillerMode;
}
/**
* Returns the currently used {@link FillerMode}.
*/
public FillerMode getFillerMode() {
return fillerMode;
}
/**
* Sets the {@link Dimension} constraints used by this panel. Each column uses exactly the
* specified dimension, no more and no less.The constraints array has to consist of n entries
* where n is the number of columns in the {@link TablePanelModel}. If the model is changed
* after this method has been called and has more or less columns than the constraints specify,
* the constraints are ignored! Set to <code>null</code> to remove the constraints. <br/>
* See {@link #setFillerMode(FillerMode)} for options to distribute additional space
*
* @param constraints
* the {@link Dimension} array consisting of n entries, where n is the number of
* columns of the {@link TablePanelModel}.
* @throws IllegalArgumentException
* if constraints.length != getModel().getColumnCount()
*/
public void setDimensionConstraints(Dimension[] constraints) throws IllegalArgumentException {
if (constraints == null) {
this.constraints = null;
return;
}
if (getModel() != null && constraints.length != getModel().getColumnCount()) {
throw new IllegalArgumentException("constraints length must match the TablePanelModel column count!");
}
for (Dimension dim : constraints) {
if (dim == null) {
throw new IllegalArgumentException("constraints element must not be null!");
}
}
this.constraints = constraints;
}
/**
* Creates the appropriate GUI component for the specified cell of the {@link TablePanelModel}.
*
* @param rowIndex
* @param columnIndex
*/
private Component createComponentForCell(final int rowIndex, final int columnIndex) {
final Class<? extends CellType> cellClass = model.getColumnClass(rowIndex, columnIndex);
// create appropriate GUI element for class
// Collections are shown via JComboBox
if (CellTypeComboBox.class.isAssignableFrom(cellClass)) {
return new CellTypeComboBoxImpl(model, rowIndex, columnIndex);
}
// Strings are shown via JTextField with Content Assist
if (CellTypeTextFieldDefault.class.isAssignableFrom(cellClass)) {
return createPanelForStrings(rowIndex, columnIndex, cellClass);
}
// Numbers are shown via JTextField with Content Assist
if (CellTypeTextFieldNumerical.class.isAssignableFrom(cellClass)) {
return createPanelForDoubles(rowIndex, columnIndex, cellClass);
}
// Times are shown via JTextField with Content Assist
if (CellTypeTextFieldTime.class.isAssignableFrom(cellClass)) {
return createPanelForStrings(rowIndex, columnIndex, cellClass);
}
// Dates are shown via JTextField with Date Picker
if (CellTypeDate.class.isAssignableFrom(cellClass)) {
return new CellTypeDateImpl(model, rowIndex, columnIndex, cellClass);
}
// DateTimes are shown via JTextField with Date Picker
if (CellTypeDateTime.class.isAssignableFrom(cellClass)) {
return new CellTypeDateImpl(model, rowIndex, columnIndex, cellClass);
}
// Regular Expressions are shown via JTextField with Regular Exrepssions Dialog
if (CellTypeRegex.class.isAssignableFrom(cellClass)) {
return new CellTypeRegexImpl(model, rowIndex, columnIndex, cellClass);
}
// Booleans are shown via JCheckBox
if (CellTypeCheckBox.class.isAssignableFrom(cellClass)) {
return new CellTypeCheckBoxImpl(model, rowIndex, columnIndex);
}
// add more GUI elements here if needed
// default fallback component is a JLabel
return createLabel(rowIndex, columnIndex);
}
/**
* Creates a {@link JLabel} for the specified cell. Does not validate the model, so make sure
* this call works!
*
* @param rowIndex
* @param columnIndex
* @return
*/
private JLabel createLabel(final int rowIndex, final int columnIndex) {
JLabel defaultLabel = new JLabel(String.valueOf(model.getValueAt(rowIndex, columnIndex)));
defaultLabel.setToolTipText(model.getHelptextAt(rowIndex, columnIndex));
return defaultLabel;
}
/**
* Creates a {@link JFormattedTextField} for the specified cell and adds it to a {@link JPanel}
* which is returned. Only allows double values as input! Does not validate the model, so make
* sure this call works!
*
* @param rowIndex
* @param columnIndex
* @param cellClass
* @return
*/
private JPanel createPanelForDoubles(final int rowIndex, final int columnIndex, final Class<? extends CellType> cellClass) {
// add a number formatter
NumberFormat format = NumberFormat.getNumberInstance(Locale.ENGLISH);
if (CellTypeTextFieldInteger.class.isAssignableFrom(cellClass)) {
format.setMinimumFractionDigits(0);
format.setMaximumFractionDigits(0);
} else {
format.setMinimumFractionDigits(1);
}
return new CellTypeTextFieldDefaultImpl(model, rowIndex, columnIndex, cellClass, null, hideUnavailableContentAssist);
}
/**
* Creates a {@link JFormattedTextField} for the specified cell and adds it to a {@link JPanel}
* which is returned. Adds content assist of applicable via
* {@link TablePanelModel#getPossibleValuesForColumnOrNull(int, int)}. Does not validate the
* model, so make sure this call works!
*
* @param rowIndex
* @param columnIndex
* @param cellClass
* @return
*/
private JPanel createPanelForStrings(final int rowIndex, final int columnIndex, final Class<? extends CellType> cellClass) {
return new CellTypeTextFieldDefaultImpl(model, rowIndex, columnIndex, cellClass, null, hideUnavailableContentAssist);
}
/**
* Updates the component in the specified cell.
*
* @param rowIndex
* @param columnIndex
*/
private void updateComponent(int rowIndex, int columnIndex) {
Pair<Integer, Integer> key = new Pair<>(rowIndex, columnIndex);
// remove old component
Component oldComponent = mapOfComponents.get(key);
if (oldComponent != null) {
innerPanel.remove(oldComponent);
}
// add updated component to panel instead
Component updatedComponent = createComponentForCell(rowIndex, columnIndex);
// add dimension constraint (if applicable)
if (isConstraintsUsed()) {
updatedComponent.setMinimumSize(constraints[columnIndex]);
updatedComponent.setMaximumSize(constraints[columnIndex]);
updatedComponent.setPreferredSize(constraints[columnIndex]);
}
if (isConstraintsUsed()) {
gbc.weightx = 0.0;
} else {
if (Collection.class.isAssignableFrom(model.getColumnClass(columnIndex))) {
gbc.weightx = 0.1;
} else {
gbc.weightx = 1.0 / model.getColumnCount();
}
}
gbc.weighty = 0.0;
if (isConstraintsUsed() && fillerMode == FillerMode.NONE) {
gbc.fill = GridBagConstraints.VERTICAL;
} else {
gbc.fill = GridBagConstraints.BOTH;
}
gbc.gridx = columnIndex;
gbc.gridy = rowIndex;
innerPanel.add(updatedComponent, gbc);
innerPanel.revalidate();
innerPanel.repaint();
mapOfComponents.put(key, updatedComponent);
}
/**
* Returns <code>true</code> if constraints are to be used; <code>false</code> otherwise.
*/
private boolean isConstraintsUsed() {
return constraints != null && constraints.length == getModel().getColumnCount();
}
}