/** * 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.studio.io.gui.internal.steps.configuration; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.List; import java.util.Set; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.ButtonGroup; import javax.swing.JCheckBoxMenuItem; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.SwingUtilities; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import com.rapidminer.core.io.data.ColumnMetaData.ColumnType; import com.rapidminer.core.io.data.DataSetException; import com.rapidminer.core.io.data.DataSetMetaData; import com.rapidminer.example.Attributes; import com.rapidminer.gui.ApplicationFrame; import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.tools.Ionicon; import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.ResourceActionAdapter; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.components.DropDownPopupButton; import com.rapidminer.gui.tools.components.DropDownPopupButton.DropDownPopupButtonBuilder; import com.rapidminer.gui.tools.dialogs.InputValidator; import com.rapidminer.studio.io.gui.internal.DataImportWizardUtils; import com.rapidminer.studio.io.gui.internal.DataWizardEventType; import com.rapidminer.tools.I18N; import com.rapidminer.tools.Observable; import com.rapidminer.tools.Observer; /** * A component that is used by the {@link ConfigureDataStep} table to display the column name, * column type and column role. Furthermore the user can change the column settings via a dropdown * menu. * * @author Nils Woehler, Marcel Michel * @since 7.0.0 * */ final class ConfigureDataTableHeader extends JPanel implements TableCellRenderer { private static final long serialVersionUID = 1L; private static final int DEFAULT_FONT_SIZE = 13; private static final String POPUP_SHOWN_COLOR = "#eb7a03"; private static final Color COLOR_COLUMN_DISABLED = new Color(154, 154, 154); private static final String ERROR_DUPLICATE_ROLE_NAME = I18N .getGUILabel("io.dataimport.step.data_column_configuration.duplicate_role_name"); private static final String ERROR_DUPLICATE_COLUMN_NAME = I18N .getGUILabel("io.dataimport.step.data_column_configuration.duplicate_column_name"); private static final String ERROR_EMPTY_COLUMN_NAME = I18N .getGUILabel("io.dataimport.step.data_column_configuration.empty_column_name"); private static final String CHANGE_TYPE_LABEL = I18N .getGUIMessage("gui.action.io.dataimport.step.data_column_configuration.type.label"); private static final String CHANGE_TYPE_TIP = I18N .getGUIMessage("gui.action.io.dataimport.step.data_column_configuration.type.tip"); private final Action changeRoleAction = new ResourceAction("io.dataimport.step.data_column_configuration.change_role") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { String columnName = metaData.getColumnMetaData(columnIndex).getName(); String type = DataImportWizardUtils.getNameForColumnType(metaData.getColumnMetaData(columnIndex).getType()); final String currentRoleName = metaData.getColumnMetaData(columnIndex).getRole(); List<String> roleList = new ArrayList<>(); if (currentRoleName != null) { roleList.add(currentRoleName); } for (String attribute : new String[] { Attributes.LABEL_NAME, Attributes.ID_NAME, Attributes.WEIGHT_NAME }) { if (attribute.equals(currentRoleName)) { continue; } roleList.add(attribute); } Object newRoleName = SwingTools.showInputDialog(ApplicationFrame.getApplicationFrame(), "io.dataimport.step.data_column_configuration.change_role", true, roleList, currentRoleName, new InputValidator<String>() { @Override public String validateInput(String input) { if (input == null) { return null; } String valueString = input.trim(); if (!valueString.equals(currentRoleName) && validator.isRoleUsed(valueString)) { return ERROR_DUPLICATE_ROLE_NAME; } return null; } }); if (newRoleName == null) { // user cancelled dialog return; } String newRoleNameString = String.valueOf(newRoleName).trim(); if (newRoleNameString.isEmpty()) { newRoleNameString = null; } if (newRoleNameString == null && currentRoleName == null || newRoleNameString != null && newRoleNameString.equals(currentRoleName)) { // user has not changed the role return; } metaData.getColumnMetaData(columnIndex).setRole(newRoleNameString); validator.validate(columnIndex); ConfigureDataTableHeader.this.setToolTipText(createTooltip(columnName, type, newRoleNameString)); ConfigureDataTableHeader.this.roleLabel.setText(newRoleNameString != null ? newRoleNameString : " "); ConfigureDataTableHeader.this.table.getTableHeader().revalidate(); ConfigureDataTableHeader.this.table.getTableHeader().repaint(); ConfigureDataTableHeader.this.table.revalidate(); ConfigureDataTableHeader.this.table.repaint(); } }; private final Action renameAction = new ResourceAction("io.dataimport.step.data_column_configuration.rename") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { final String currentColumnName = metaData.getColumnMetaData(columnIndex).getName(); String type = DataImportWizardUtils.getNameForColumnType(metaData.getColumnMetaData(columnIndex).getType()); String roleName = metaData.getColumnMetaData(columnIndex).getRole(); String newColumnName = SwingTools.showInputDialog(ApplicationFrame.getApplicationFrame(), "io.dataimport.step.data_column_configuration.rename", currentColumnName, new InputValidator<String>() { @Override public String validateInput(String inputString) { if (inputString == null || inputString.trim().isEmpty()) { return ERROR_EMPTY_COLUMN_NAME; } else if (!inputString.trim().equals(currentColumnName) && validator.isNameUsed(inputString.trim())) { return ERROR_DUPLICATE_COLUMN_NAME; } else { return null; } } }); if (newColumnName == null || newColumnName.trim().equals(currentColumnName)) { // user cancelled dialog or did not change the name return; } newColumnName = newColumnName.trim(); metaData.getColumnMetaData(columnIndex).setName(newColumnName); validator.validate(columnIndex); ConfigureDataTableHeader.this.setToolTipText(createTooltip(newColumnName, type, roleName)); ConfigureDataTableHeader.this.nameLabel.setText(newColumnName); ConfigureDataTableHeader.this.table.getTableHeader().revalidate(); ConfigureDataTableHeader.this.table.getTableHeader().repaint(); } }; private final Action disableEnableAction = new ResourceAction("io.dataimport.step.data_column_configuration.disable") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { metaData.getColumnMetaData(columnIndex).setRemoved(!metaData.getColumnMetaData(columnIndex).isRemoved()); if (metaData.getColumnMetaData(columnIndex).isRemoved()) { nameLabel.setForeground(COLOR_COLUMN_DISABLED); typeLabel.setForeground(COLOR_COLUMN_DISABLED); roleLabel.setForeground(COLOR_COLUMN_DISABLED); } else { nameLabel.setForeground(Color.BLACK); typeLabel.setForeground(Color.BLACK); roleLabel.setForeground(Color.BLACK); } validator.validate(columnIndex); updateDisableEnableAction(); ConfigureDataTableHeader.this.table.revalidate(); ConfigureDataTableHeader.this.table.repaint(); } }; private final JTable table; private final DropDownPopupButton configureColumnButton; private final int columnIndex; private final DataSetMetaData metaData; private final ConfigureDataValidator validator; private final JLabel nameLabel; private final JLabel roleLabel; private final JLabel typeLabel; private final ConfigureDataView configureDataView; public ConfigureDataTableHeader(final JTable table, final int columnIndex, final DataSetMetaData metaData, final ConfigureDataValidator validator, final ConfigureDataView configureDataView) { this.table = table; this.columnIndex = columnIndex; this.validator = validator; this.metaData = metaData; this.configureDataView = configureDataView; validator.addObserver(new Observer<Set<Integer>>() { @Override public void update(Observable<Set<Integer>> observable, Set<Integer> arg) { if (arg != null && arg.contains(columnIndex)) { adjustErrorColors(); } } }, false); setLayout(new GridBagLayout()); setBackground(Colors.TABLE_HEADER_BACKGROUND_GRADIENT_START); setBorder(BorderFactory.createLineBorder(Colors.TABLE_HEADER_BORDER)); setMinimumSize(new Dimension(120, 50)); String columnName = metaData.getColumnMetaData(columnIndex).getName(); String type = DataImportWizardUtils.getNameForColumnType(metaData.getColumnMetaData(columnIndex).getType()); String roleName = metaData.getColumnMetaData(columnIndex).getRole(); setToolTipText(createTooltip(columnName, type, roleName)); GridBagConstraints gbc = new GridBagConstraints(); // add name label { nameLabel = new JLabel(columnName); nameLabel.setFont(nameLabel.getFont().deriveFont(Font.BOLD, DEFAULT_FONT_SIZE)); gbc.gridwidth = GridBagConstraints.RELATIVE; gbc.insets = new Insets(0, 5, 0, 0); gbc.weightx = 1.0; gbc.fill = GridBagConstraints.HORIZONTAL; add(nameLabel, gbc); } // add drop down button { updateDisableEnableAction(); configureColumnButton = new DropDownPopupButtonBuilder() .with(new ResourceActionAdapter(true, "io.dataimport.step.data_column_configuration.header_action")) .add(createTypeMenu()).add(changeRoleAction).add(renameAction).add(disableEnableAction).build(); configureColumnButton.setIcon(null); configureColumnButton.setBorder(null); configureColumnButton.setOpaque(false); configureColumnButton.setContentAreaFilled(false); configureColumnButton.setBorderPainted(false); configureColumnButton.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { configureColumnButton.setColor(POPUP_SHOWN_COLOR); table.getTableHeader().repaint(); } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { configureColumnButton.setColor(null); table.getTableHeader().repaint(); } @Override public void popupMenuCanceled(PopupMenuEvent e) {} }); configureColumnButton.setArrowSize(DEFAULT_FONT_SIZE); configureColumnButton.setTextSize(DEFAULT_FONT_SIZE); configureColumnButton.setText(Ionicon.GEAR_B.getHtml()); gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.insets = new Insets(0, 0, 0, 0); gbc.weightx = 0.0; gbc.fill = GridBagConstraints.NONE; add(configureColumnButton, gbc); } // add type label { typeLabel = new JLabel(type); typeLabel.setFont(typeLabel.getFont().deriveFont(Font.ITALIC)); gbc.weightx = 1.0; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.insets = new Insets(0, 5, 0, 0); add(typeLabel, gbc); } // add role label { roleLabel = new JLabel(roleName != null ? roleName : " "); roleLabel.setFont(roleLabel.getFont().deriveFont(Font.ITALIC)); add(roleLabel, gbc); } adjustErrorColors(); setupMouseListener(); } /** * @return menu that allows to select possible column types */ private JMenu createTypeMenu() { ButtonGroup typeGroup = new ButtonGroup(); JMenu typeChangeItem = new JMenu(CHANGE_TYPE_LABEL); typeChangeItem.setToolTipText(CHANGE_TYPE_TIP); for (final ColumnType columnType : ColumnType.values()) { final JCheckBoxMenuItem checkboxItem = new JCheckBoxMenuItem( DataImportWizardUtils.getNameForColumnType(columnType)); if (columnType == metaData.getColumnMetaData(columnIndex).getType()) { checkboxItem.setSelected(true); } checkboxItem.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { changeType(columnType); } } }); typeGroup.add(checkboxItem); typeChangeItem.add(checkboxItem); } return typeChangeItem; } /** * Adds to the table header a {@link MouseListener} which manages the * {@link #configureColumnButton} action. */ private void setupMouseListener() { table.getTableHeader().addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { JTableHeader header = ConfigureDataTableHeader.this.table.getTableHeader(); // this call is very expensive for many columns, because the // default model iterates over every column and computes the corresponding width int currentIndex = header.getColumnModel().getColumnIndexAtX(e.getPoint().x); if (currentIndex != columnIndex || currentIndex == -1) { return; } Rectangle headerRec = header.getHeaderRect(currentIndex); setBounds(headerRec); header.add(ConfigureDataTableHeader.this); validate(); Rectangle buttonRec = configureColumnButton.getBounds(null); buttonRec.x += headerRec.x; buttonRec.y += headerRec.y; Rectangle nameRec = nameLabel.getBounds(null); nameRec.x += headerRec.x; nameRec.y += headerRec.y; if (buttonRec.contains(e.getPoint())) { configureColumnButton.doClick(); } else if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() >= 2 && nameRec.contains(e.getPoint())) { renameAction.actionPerformed(null); } header.remove(ConfigureDataTableHeader.this); header.repaint(); } }); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) { return this; } /** * Color the name or role red if it is a duplicate. */ private void adjustErrorColors() { if (metaData.getColumnMetaData(columnIndex).isRemoved()) { nameLabel.setForeground(COLOR_COLUMN_DISABLED); roleLabel.setForeground(COLOR_COLUMN_DISABLED); typeLabel.setForeground(COLOR_COLUMN_DISABLED); } else { Color nameColor = validator.isDuplicateNameColumn(columnIndex) ? Color.RED : Color.BLACK; nameLabel.setForeground(nameColor); Color roleColor = validator.isDuplicateRoleColumn(columnIndex) ? Color.RED : Color.BLACK; roleLabel.setForeground(roleColor); } } /** * Updates the {@link #disableEnableAction} in regard to the {@link #metaData}. */ private void updateDisableEnableAction() { if (metaData.getColumnMetaData(columnIndex).isRemoved()) { disableEnableAction.putValue(Action.NAME, I18N.getGUIMessage("gui.action.io.dataimport.step.data_column_configuration.enable.label")); disableEnableAction.putValue(Action.SHORT_DESCRIPTION, I18N.getGUIMessage("gui.action.io.dataimport.step.data_column_configuration.enable.tip")); } else { disableEnableAction.putValue(Action.NAME, I18N.getGUIMessage("gui.action.io.dataimport.step.data_column_configuration.disable.label")); disableEnableAction.putValue(Action.SHORT_DESCRIPTION, I18N.getGUIMessage("gui.action.io.dataimport.step.data_column_configuration.disable.tip")); } } /** * Creates the tooltip text for the table header. * * @param columnName * the human readable name of the column * @param type * the human readable attribute type * @param roleName * the human readable role name * @return the created tooltip text */ private static String createTooltip(String columnName, String type, String roleName) { // build header tooltip StringBuilder tipBuilder = new StringBuilder(); tipBuilder.append("<html><table><tbody><tr><td><strong>"); tipBuilder.append(I18N.getGUILabel("io.dataimport.step.data_column_configuration.tooltip_name")); tipBuilder.append("</strong><td><td>"); tipBuilder.append(columnName); tipBuilder.append("<td></tr><tr><td><strong>"); tipBuilder.append(I18N.getGUILabel("io.dataimport.step.data_column_configuration.tooltip_type")); tipBuilder.append("</strong><td><td>"); tipBuilder.append(type); tipBuilder.append("<td></tr>"); if (roleName != null) { tipBuilder.append("<tr><td><strong>"); tipBuilder.append(I18N.getGUILabel("io.dataimport.step.data_column_configuration.tooltip_role")); tipBuilder.append("</strong><td><td>"); tipBuilder.append(roleName); tipBuilder.append("<td></tr>"); } tipBuilder.append("</tbody></table></html>"); return tipBuilder.toString(); } /** * Updates the column type to the newType. Rereads the column and updates the error table. * * @param newType * the new column type */ private void changeType(final ColumnType newType) { DataImportWizardUtils.logStats(DataWizardEventType.COLUMN_TYPE_CHANGED, metaData.getColumnMetaData(columnIndex).getType() + "->" + newType); metaData.getColumnMetaData(columnIndex).setType(newType); final ConfigureDataTableModel tableModel = (ConfigureDataTableModel) ConfigureDataTableHeader.this.table.getModel(); ProgressThread columnThread = new ProgressThread("io.dataimport.step.data_column_configuration.update_column") { @Override public void run() { try { tableModel.rereadColumn(columnIndex, getProgressListener()); } catch (final DataSetException e) { SwingTools.invokeLater(new Runnable() { @Override public void run() { configureDataView.showErrorNotification( "io.dataimport.step.data_column_configuration.error_loading_data", e.getMessage()); } }); return; } SwingTools.invokeLater(new Runnable() { @Override public void run() { validator.setParsingErrors(tableModel.getParsingErrors()); ConfigureDataTableHeader.this .setToolTipText(createTooltip(metaData.getColumnMetaData(columnIndex).getName(), DataImportWizardUtils.getNameForColumnType(newType), metaData.getColumnMetaData(columnIndex).getRole())); ConfigureDataTableHeader.this.typeLabel.setText(DataImportWizardUtils.getNameForColumnType(newType)); ConfigureDataTableHeader.this.table.repaint(); ConfigureDataTableHeader.this.table.getTableHeader().repaint(); } }); } }; columnThread.start(); } }