/**
* 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.data.internal.file.excel;
import java.awt.BorderLayout;
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.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.OverlayLayout;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableModel;
import com.rapidminer.core.io.gui.InvalidConfigurationException;
import com.rapidminer.gui.look.Colors;
import com.rapidminer.gui.tools.ColoredTableCellRenderer;
import com.rapidminer.gui.tools.ExtendedJScrollPane;
import com.rapidminer.gui.tools.ExtendedJTable;
import com.rapidminer.gui.tools.ResourceActionAdapter;
import com.rapidminer.gui.tools.ResourceLabel;
import com.rapidminer.gui.tools.RowNumberTable;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.gui.tools.bubble.BubbleWindow;
import com.rapidminer.gui.tools.bubble.BubbleWindow.AlignedSide;
import com.rapidminer.gui.tools.bubble.BubbleWindow.BubbleStyle;
import com.rapidminer.gui.tools.bubble.ComponentBubbleWindow;
import com.rapidminer.operator.nio.model.xlsx.XlsxSheetMetaDataParser;
import com.rapidminer.operator.nio.model.xlsx.XlsxUtilities;
import com.rapidminer.operator.nio.model.xlsx.XlsxUtilities.XlsxCellCoordinates;
import com.rapidminer.studio.io.data.internal.ResultSetAdapter;
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.LogService;
import com.rapidminer.tools.container.Pair;
/**
* This is a panel showing the contents of a complete excel workbook. The user is able to select the
* current sheet via a drop down menu.
*
* @author Nils Woehler
* @since 7.0.0
*/
final class ExcelSheetSelectionPanel extends JPanel {
private static final long serialVersionUID = 9179757216097316344L;
private static final Dimension CELL_TEXTFIELD_MIN_DIMENSION = new Dimension(50, 25);
private static final Dimension CELL_TEXTFIELD_PREF_DIMENSION = new Dimension(150, 25);
private static final Dimension CELL_CHECKBOX_MIN_DIMENSION = new Dimension(100, 25);
/** re-use same log timer for all data import wizard dialogs */
private static final Timer CELL_RANGE_TIMER = new Timer(500, null);
/**
* Cell renderer for the content table.
*/
private final ColoredTableCellRenderer tableCellRenderer = new ColoredTableCellRenderer() {
private final DefaultTableCellRenderer defaultRenderer = new DefaultTableCellRenderer();
private final Font normalFont = defaultRenderer.getFont();
private final Font boldFont = defaultRenderer.getFont().deriveFont(Font.BOLD);
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
int row, int column) {
Component rendererComponent = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row,
column);
try {
if (row == getHeaderRowIndex()) {
rendererComponent.setFont(boldFont);
} else {
rendererComponent.setFont(normalFont);
}
} catch (NumberFormatException e) {
rendererComponent.setFont(normalFont);
}
if (sheetSelectionModel.isShowingPreview()) {
try {
CellRangeSelection selection = getSelection();
// highlight preview cells as if they were selected if included in the selection
if (row >= selection.getRowIndexStart() && row <= selection.getRowIndexEnd()
&& column >= selection.getColumnIndexStart() && column <= selection.getColumnIndexEnd()) {
rendererComponent.setBackground(contentTable.getSelectionBackground());
rendererComponent.setForeground(contentTable.getSelectionForeground());
} else {
rendererComponent.setBackground(contentTable.getBackground());
rendererComponent.setForeground(contentTable.getForeground());
}
} catch (InvalidConfigurationException e) {
rendererComponent.setBackground(contentTable.getBackground());
rendererComponent.setForeground(contentTable.getForeground());
}
}
return rendererComponent;
}
};
/**
* Selection listener which updates the sheet selection model in case of list selection events.
*/
private final ListSelectionListener selectionListener = new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting() && !updatingUI) {
sheetSelectionModel.updateCellRangeByTableSelection(contentTable);
}
}
};
/**
* Selection listener which updates the sheet selection model in case of column selection
* events.
*/
private final TableColumnModelListener columnSelectionListener = new TableColumnModelListener() {
@Override
public void columnAdded(TableColumnModelEvent e) {}
@Override
public void columnRemoved(TableColumnModelEvent e) {}
@Override
public void columnMoved(TableColumnModelEvent e) {}
@Override
public void columnMarginChanged(ChangeEvent e) {}
@Override
public void columnSelectionChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting() && !updatingUI) {
sheetSelectionModel.updateCellRangeByTableSelection(contentTable);
}
}
};
/**
* Action which selects the whole sheet content.
*/
private final Action selectAllAction = new ResourceActionAdapter(false,
"io.dataimport.step.excel.sheet_selection.select_all") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
killCurrentBubbleWindow(null);
// create a new selection object
CellRangeSelection selection = new CellRangeSelection(0, XlsxCellCoordinates.NO_ROW_NUMBER,
contentTable.getModel().getColumnCount() - 1, XlsxSheetMetaDataParser.MAXIMUM_XLSX_ROW_INDEX);
// update the model (will trigger an UI update)
sheetSelectionModel.setCellRangeSelection(selection);
}
};
/**
* Action to apply the selection entered into the cell range text field.
*/
private final Action applySelectionAction = new ResourceActionAdapter(false,
"io.dataimport.step.excel.sheet_selection.apply_cell_selection") {
private static final long serialVersionUID = 1L;
@Override
public synchronized void actionPerformed(ActionEvent e) {
applyTextFieldSelection(true);
}
};
/**
* Item listener that acts in case the sheet has been changed.
*/
private final ItemListener comboBoxItemListener = new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED) {
if (!updatingUI) {
int newSheetIndex = sheetComboBox.getSelectedIndex();
sheetSelectionModel.setSheetIndex(newSheetIndex);
}
}
}
};
/**
* Observer that watches the {@link ExcelSheetSelectionPanelModel} and updates the UI on
* changes.
*/
private final ExcelSheetSelectionModelListener sheetSelectionModelListener = new ExcelSheetSelectionModelListener() {
@Override
public void loadingNewTableModel() {
SwingTools.invokeAndWait(new Runnable() {
@Override
public void run() {
updatingUI = true;
showNotificationLabel("io.dataimport.step.excel.sheet_selection.loading_excel_sheets");
fireStateChanged();
updatingUI = false;
}
});
}
@Override
public void reportErrorLoadingTableModel(final Exception e) {
SwingTools.invokeAndWait(new Runnable() {
@Override
public void run() {
showNotificationLabel("io.dataimport.step.excel.sheet_selection.error_loading_excel_sheet",
e.getMessage());
fireStateChanged();
}
});
}
@Override
public void sheetIndexUpdated(final int newSheetIndex, final String[] sheetNames, final TableModel tableModel,
final boolean isShowingPreview, final boolean wasModelLoaded) {
updatingUI = true;
SwingTools.invokeAndWait(new Runnable() {
@Override
public void run() {
fireStateChanged();
updateTableModel(tableModel, isShowingPreview, wasModelLoaded, sheetNames, newSheetIndex);
}
});
updatingUI = false;
fireStateChanged();
}
@Override
public void headerRowIndexUpdated(final int newHeaderRowIndex) {
updatingUI = true;
SwingTools.invokeAndWait(new Runnable() {
@Override
public void run() {
boolean hasHeaderRow = newHeaderRowIndex > ResultSetAdapter.NO_HEADER_ROW;
int displayedHeaderRowIndex = hasHeaderRow ? newHeaderRowIndex + 1 : 1;
headerRowSpinner.setModel(new SpinnerNumberModel(displayedHeaderRowIndex, 1, Integer.MAX_VALUE, 1));
hasHeaderRowCheckBox.setSelected(hasHeaderRow);
killCurrentBubbleWindow(headerRowSpinner);
contentTable.revalidate();
contentTable.repaint();
}
});
updatingUI = false;
fireStateChanged();
}
@Override
public void cellRangeSelectionUpdate(final CellRangeSelection newSelection) {
updatingUI = true;
SwingTools.invokeAndWait(new Runnable() {
@Override
public void run() {
if (newSelection == null) {
contentTable.clearSelection();
return;
}
// column indices
int columnStartIndex = Math.max(newSelection.getColumnIndexStart(), 0);
int columnEndIndex = Math.min(newSelection.getColumnIndexEnd(),
isSheetEmpty() ? 0 : contentTable.getColumnCount() - 1);
// row indices
int firstSelectedRow = newSelection.getRowIndexStart();
int lastSelectedRow = newSelection.getRowIndexEnd();
if (sheetSelectionModel.isShowingPreview()) {
// Only update the text field when showing a preview
updateCellRangeTextFields(columnStartIndex, firstSelectedRow, columnEndIndex, lastSelectedRow);
} else {
if (!isSheetEmpty()) {
contentTable.clearSelection();
int tableRowCount = isSheetEmpty() ? 0 : contentTable.getRowCount() - 1;
int lastSelectedTableRow = Math.min(newSelection.getRowIndexEnd(), tableRowCount);
// no row number means we start with first row for table selection
int firstSelectedTableRow = firstSelectedRow == XlsxCellCoordinates.NO_ROW_NUMBER ? 0
: firstSelectedRow;
// update the table selection
contentTable.setColumnSelectionInterval(columnStartIndex, columnEndIndex);
contentTable.setRowSelectionInterval(firstSelectedTableRow, lastSelectedTableRow);
// update the text field
updateCellRangeTextFields(columnStartIndex, firstSelectedRow, columnEndIndex, lastSelectedRow);
}
}
}
});
updatingUI = false;
fireStateChanged();
contentTable.revalidate();
contentTable.repaint();
}
private void updateTableModel(TableModel selectedModel, boolean isShowingPreview, boolean wasModelLoaded,
String[] sheetNames, int sheetIndex) {
final GridBagConstraints constraint = new GridBagConstraints();
constraint.fill = GridBagConstraints.BOTH;
constraint.weightx = 1.0;
constraint.weighty = 1.0;
// create a new table
contentTable = new ExtendedJTable(false, false, false) {
private static final long serialVersionUID = 1L;
@Override
public void changeSelection(int rowIndex, int columnIndex, boolean toggle, boolean extend) {
/*
* Overwrite selection change event for the sheet table to disable partial
* selection changing with CTRL + click.
*/
if (toggle && !extend) {
return;
}
super.changeSelection(rowIndex, columnIndex, toggle, extend);
};
};
// ensure same background as JPanels in case of only few rows
contentTable.setBackground(Colors.PANEL_BACKGROUND);
contentTable.setGridColor(Colors.TAB_BORDER);
contentTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
contentTable.setBorder(null);
contentTable.getSelectionModel().addListSelectionListener(selectionListener);
contentTable.getColumnModel().addColumnModelListener(columnSelectionListener);
// only allow selection in case no preview is shown
contentTable.setRowSelectionAllowed(!isShowingPreview);
contentTable.setColumnSelectionAllowed(!isShowingPreview);
contentTable.setCellSelectionEnabled(!isShowingPreview);
contentTable.setShowPopupMenu(false);
contentTable.setSortable(false);
contentTable.getTableHeader().setReorderingAllowed(false);
contentTable.setColoredTableCellRenderer(tableCellRenderer);
contentTable.setEnabled(!isShowingPreview);
// update table model
contentTable.setModel(selectedModel);
// update combo box content
sheetComboBox.setModel(new DefaultComboBoxModel<String>(sheetNames));
sheetComboBox.setSelectedIndex(sheetIndex);
// add content table again
centerPanel.removeAll();
/*
* Hack to enlarge table columns in case of few columns. Add table to a full size JPanel
* and add the table header to the scroll pane.
*/
JPanel tablePanel = new JPanel(new BorderLayout());
tablePanel.add(contentTable, BorderLayout.CENTER);
JScrollPane scrollPane = new ExtendedJScrollPane(tablePanel);
scrollPane.setColumnHeaderView(contentTable.getTableHeader());
// Add scroll panel and table
scrollPane.setRowHeaderView(new RowNumberTable(contentTable));
scrollPane.setBorder(null);
if (isShowingPreview) {
// Create a layered pane to display both, the data table and a
// "preview" overlay
JLayeredPane layeredPane = new JLayeredPane();
layeredPane.setLayout(new OverlayLayout(layeredPane));
// add scroll pane
layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER);
// Add "Preview" overlay
JPanel previewPanel = new JPanel(new BorderLayout());
previewPanel.setOpaque(false);
JLabel previewLabel = new JLabel(I18N.getGUILabel("csv_format_specification.preview_background"),
SwingConstants.CENTER);
previewLabel.setFont(previewLabel.getFont().deriveFont(Font.BOLD, 180));
previewLabel.setForeground(DataImportWizardUtils.getPreviewFontColor());
previewPanel.add(previewLabel, BorderLayout.CENTER);
layeredPane.add(previewPanel, JLayeredPane.PALETTE_LAYER);
centerPanel.add(layeredPane, constraint);
} else {
// display plain scroll pane
centerPanel.add(scrollPane, constraint);
}
/*
* one last check whether the selected sheet is empty
*/
if (isSheetEmpty()) {
showNotificationLabel("io.dataimport.step.excel.sheet_selection.empty_sheet");
// even though the sheet is empty the user should still be able
// to
// select another sheet
sheetComboBox.setEnabled(true);
} else {
// enable all header actions
enableHeaderActions(true);
}
/*
* Show an information bubble in case the modal was loaded the first time and only a
* preview is shown.
*/
if (isShowingPreview && wasModelLoaded) {
createBubbleWindow(cellRangeTextField, BubbleStyle.WARNING,
"io.dataimport.step.excel.sheet_selection.showing_preview", XlsxUtilities.getSheetSelectionLength());
}
contentTable.revalidate();
contentTable.repaint();
}
/**
* Uses the provided start and end ranges to configure the {@link #cellRangeTextField}.
*
* @param columnIndexStart
* the 0-based column start index
* @param rowIndexStart
* the 0-base row start index, {@link XlsxCellCoordinates#NO_ROW_NUMBER} means
* the first start row is {@code 0}
* @param columnIndexEnd
* the 0-based index that defines last column to import
* @param rowIndexEnd
* the 0-based index that defines the last row to import
*/
private void updateCellRangeTextFields(int columnIndexStart, int rowIndexStart, int columnIndexEnd,
int rowIndexEnd) {
try {
String startCell;
if (rowIndexStart == XlsxCellCoordinates.NO_ROW_NUMBER
&& rowIndexEnd == XlsxSheetMetaDataParser.MAXIMUM_XLSX_ROW_INDEX) {
startCell = XlsxUtilities.convertToColumnName(columnIndexStart);
} else if (rowIndexStart == XlsxCellCoordinates.NO_ROW_NUMBER) {
startCell = XlsxUtilities.convertToColumnName(columnIndexStart) + "1";
} else {
startCell = XlsxUtilities.convertToColumnName(columnIndexStart) + (rowIndexStart + 1);
}
String endCell;
if (rowIndexEnd == XlsxSheetMetaDataParser.MAXIMUM_XLSX_ROW_INDEX) {
endCell = XlsxUtilities.convertToColumnName(columnIndexEnd);
} else {
endCell = XlsxUtilities.convertToColumnName(columnIndexEnd) + (rowIndexEnd + 1);
}
cellRangeTextField.setText(startCell + ":" + endCell);
} catch (IllegalArgumentException e) {
cellRangeTextField.setText("");
}
}
};
/** the model for the sheet selection defined in this step */
private final ExcelSheetSelectionPanelModel sheetSelectionModel;
/*
* UI elements
*/
private final JComboBox<String> sheetComboBox = new JComboBox<>();
private final JPanel centerPanel = new JPanel(new GridBagLayout());
private final JTextField cellRangeTextField = new JTextField();
private final JCheckBox hasHeaderRowCheckBox = new JCheckBox(
I18N.getGUILabel("io.dataimport.step.excel.sheet_selection.use_header_row"), true);
private final JSpinner headerRowSpinner = new JSpinner();
private ExtendedJTable contentTable = new ExtendedJTable();
private BubbleWindow currentBubbleWindow;
private JComponent bubbleOwner;
/**
* Flag to avoid endless loop caused by e.g. the ComboBox listener and other listeners during UI
* update.
*/
private boolean updatingUI = false;
private List<ChangeListener> changeListeners = new LinkedList<>();
public ExcelSheetSelectionPanel(ExcelDataSource dataSource) {
this.sheetSelectionModel = new ExcelSheetSelectionPanelModel(dataSource, sheetSelectionModelListener);
setLayout(new BorderLayout());
// create north panel
{
GridBagConstraints constraint = new GridBagConstraints();
constraint.gridx = 0;
constraint.insets = new Insets(0, 0, 0, 30);
constraint.weightx = 0.0;
constraint.fill = GridBagConstraints.NONE;
JPanel northPanel = new JPanel(new GridBagLayout());
northPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 15, 10));
// add sheet panel
{
GridBagConstraints innerGbc = new GridBagConstraints();
innerGbc.insets = new Insets(5, 5, 5, 0);
JPanel sheetPanel = new JPanel(new GridBagLayout());
innerGbc.gridx = 0;
sheetPanel.add(new ResourceLabel("io.dataimport.step.excel.sheet_selection.use_sheet"), innerGbc);
innerGbc.gridx += 1;
sheetPanel.add(sheetComboBox, innerGbc);
sheetComboBox.addItemListener(comboBoxItemListener);
northPanel.add(sheetPanel, constraint);
}
// cell range panel
{
GridBagConstraints innerGbc = new GridBagConstraints();
innerGbc.insets = new Insets(5, 5, 5, 0);
JPanel cellRangeColumnPanel = new JPanel(new GridBagLayout());
innerGbc.gridx = 0;
cellRangeColumnPanel.add(new ResourceLabel("io.dataimport.step.excel.sheet_selection.cells"), innerGbc);
innerGbc.gridx += 1;
cellRangeTextField.setMinimumSize(CELL_TEXTFIELD_MIN_DIMENSION);
cellRangeTextField.setPreferredSize(CELL_TEXTFIELD_PREF_DIMENSION);
cellRangeTextField.addActionListener(applySelectionAction);
cellRangeTextField.getDocument().addUndoableEditListener(new UndoableEditListener() {
@Override
public void undoableEditHappened(UndoableEditEvent e) {
if (!updatingUI) {
// kill any possible error bubble window
killCurrentBubbleWindow(null);
// apply text field input
applyTextFieldSelection(false);
// remove all action listeners
for (ActionListener l : CELL_RANGE_TIMER.getActionListeners()) {
CELL_RANGE_TIMER.removeActionListener(l);
}
// add new action listener
CELL_RANGE_TIMER.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
applyTextFieldSelection(true);
}
});
// restart timer (show an error after 1500 seconds if the input is
// erroneous and the user does not enter any new text)
CELL_RANGE_TIMER.setRepeats(false);
CELL_RANGE_TIMER.restart();
}
}
});
cellRangeColumnPanel.add(cellRangeTextField, innerGbc);
innerGbc.gridx += 1;
cellRangeColumnPanel.add(new JButton(selectAllAction), innerGbc);
constraint.gridx += 1;
northPanel.add(cellRangeColumnPanel, constraint);
}
// header configuration
{
GridBagConstraints innerGbc = new GridBagConstraints();
innerGbc.insets = new Insets(5, 5, 5, 0);
JPanel headerConfigurationPanel = new JPanel(new GridBagLayout());
hasHeaderRowCheckBox.setMinimumSize(CELL_CHECKBOX_MIN_DIMENSION);
hasHeaderRowCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
DataImportWizardUtils.logStats(DataWizardEventType.EXCEL_HEADER_ROW_STATE,
Boolean.toString(hasHeaderRowCheckBox.isSelected()));
headerRowSpinner.setEnabled(hasHeaderRowCheckBox.isSelected());
if (!hasHeaderRowCheckBox.isSelected()) {
sheetSelectionModel.setHeaderRowIndex(ResultSetAdapter.NO_END_ROW);
} else {
sheetSelectionModel.setHeaderRowIndex(getHeaderRowIndexFromSpinner());
}
}
});
innerGbc.gridx = 0;
headerConfigurationPanel.add(hasHeaderRowCheckBox, innerGbc);
headerRowSpinner.setModel(new SpinnerNumberModel(1, 1, Integer.MAX_VALUE, 1));
headerRowSpinner.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
sheetSelectionModel.setHeaderRowIndex(getHeaderRowIndexFromSpinner());
}
});
innerGbc.gridx += 1;
headerConfigurationPanel.add(headerRowSpinner, innerGbc);
constraint.gridx += 1;
northPanel.add(headerConfigurationPanel, constraint);
}
constraint.weightx = 1.0;
constraint.fill = GridBagConstraints.HORIZONTAL;
constraint.gridx += 1;
northPanel.add(new JLabel(), constraint);
add(northPanel, BorderLayout.NORTH);
}
// add center panel
add(centerPanel, BorderLayout.CENTER);
// initialize the model
sheetSelectionModel.setSheetIndex(0);
}
private void applyTextFieldSelection(boolean showBubbleOnError) {
try {
Pair<XlsxCellCoordinates, XlsxCellCoordinates> coordinates = getCoordinates();
XlsxCellCoordinates fromCoordinates = coordinates.getFirst();
XlsxCellCoordinates toCoordinates = coordinates.getSecond();
boolean errorDetected = false;
if (fromCoordinates.columnNumber > toCoordinates.columnNumber) {
sheetSelectionModel.setCellRangeSelection(null);
if (showBubbleOnError) {
createBubbleWindow(cellRangeTextField, BubbleStyle.ERROR,
"io.dataimport.step.excel.sheet_selection.invalid_range");
}
errorDetected = true;
} else if (fromCoordinates.rowNumber > toCoordinates.rowNumber) {
sheetSelectionModel.setCellRangeSelection(null);
if (showBubbleOnError) {
createBubbleWindow(cellRangeTextField, BubbleStyle.ERROR,
"io.dataimport.step.excel.sheet_selection.invalid_range");
}
errorDetected = true;
} else if (fromCoordinates.columnNumber >= contentTable.getColumnCount()) {
sheetSelectionModel.setCellRangeSelection(null);
if (showBubbleOnError) {
createBubbleWindow(cellRangeTextField, BubbleStyle.ERROR,
"io.dataimport.step.excel.sheet_selection.invalid_range");
}
errorDetected = true;
}
if (!errorDetected) {
// create new selection object
CellRangeSelection newSelection = new CellRangeSelection(fromCoordinates.columnNumber,
fromCoordinates.rowNumber, toCoordinates.columnNumber, toCoordinates.rowNumber);
// update the model (will trigger an UI update)
sheetSelectionModel.setCellRangeSelection(newSelection);
} else {
sheetSelectionModel.setCellRangeSelection(null);
}
} catch (InvalidConfigurationException e1) {
sheetSelectionModel.setCellRangeSelection(null);
if (showBubbleOnError) {
createBubbleWindow(cellRangeTextField, BubbleStyle.ERROR,
"io.dataimport.step.excel.sheet_selection.wrong_cell_format", cellRangeTextField.getText());
}
} finally {
// stop timer
CELL_RANGE_TIMER.stop();
}
}
private Pair<XlsxCellCoordinates, XlsxCellCoordinates> getCoordinates() throws InvalidConfigurationException {
XlsxCellCoordinates fromCoordinates = getFromCellCoordinates();
XlsxCellCoordinates toCoordinates = getToCellCoordinates();
return new Pair<>(fromCoordinates, toCoordinates);
}
private XlsxCellCoordinates getFromCellCoordinates() throws InvalidConfigurationException {
try {
return XlsxUtilities.convertCellRefToCoordinates(getCellRangeArguments()[0]);
} catch (IllegalArgumentException e) {
throw new InvalidConfigurationException();
}
}
private XlsxCellCoordinates getToCellCoordinates() throws InvalidConfigurationException {
try {
XlsxCellCoordinates toCoordinates = XlsxUtilities.convertCellRefToCoordinates(getCellRangeArguments()[1]);
// replace no row number flag by all rows flag
if (toCoordinates.rowNumber == XlsxCellCoordinates.NO_ROW_NUMBER) {
toCoordinates.rowNumber = XlsxSheetMetaDataParser.MAXIMUM_XLSX_ROW_INDEX;
}
return toCoordinates;
} catch (IllegalArgumentException e) {
throw new InvalidConfigurationException();
}
}
private String[] getCellRangeArguments() throws InvalidConfigurationException {
String[] cellRanges = cellRangeTextField.getText().split(":");
if (cellRanges.length != 2) {
throw new InvalidConfigurationException();
}
return cellRanges;
}
/**
* @return the header row index selected by the header row spinner. It is always one value below
* the shown line number.
*/
private int getHeaderRowIndexFromSpinner() {
return Integer.parseInt(headerRowSpinner.getValue().toString()) - 1;
}
/**
*
* Configures the UI according to the configuration of the provided {@link ExcelDataSource}.
*
* @param excelDataSource
* the {@link ExcelDataSource} that should be used to configure the UI
*/
void configureSheetSelectionModel(final ExcelDataSource excelDataSource) {
updateModel(excelDataSource.getResultSetConfiguration().getSheet(), excelDataSource.getHeaderRowIndex(),
new CellRangeSelection(excelDataSource.getResultSetConfiguration()));
}
/**
* Kills the current error bubble.
*
* @param owner
* the owner of the error, if the actual bubble owner is different this method won't
* do anything. If the given owner is {@code null} the bubble will be forcefully
* killed without respect to any owner.
*/
void killCurrentBubbleWindow(JComponent owner) {
if (currentBubbleWindow != null && (owner == null || owner == bubbleOwner)) {
currentBubbleWindow.killBubble(true);
currentBubbleWindow = null;
bubbleOwner = null;
}
}
/**
* Creates a bubble for the component and kills other bubbles.
*
* @param component
* the component for which to show the bubble
* @param style
* the bubble style
* @param i18n
* the i18n key
* @param arguments
* arguments for the i18n
*/
private void createBubbleWindow(JComponent component, BubbleStyle style, String i18n, Object... arguments) {
killCurrentBubbleWindow(null);
bubbleOwner = component;
JButton okayButton = new JButton(I18N.getGUILabel("io.dataimport.step.excel.sheet_selection.got_it"));
final ComponentBubbleWindow errorWindow = new ComponentBubbleWindow(component, style,
SwingUtilities.getWindowAncestor(ExcelSheetSelectionPanel.this), AlignedSide.TOP, i18n, null, null, false,
true, new JButton[] { okayButton }, arguments);
okayButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
errorWindow.killBubble(false);
}
});
// show and remember error window
errorWindow.setVisible(true);
currentBubbleWindow = errorWindow;
};
/**
* Shows a "header row behind start row"-bubble
*/
void notifyHeaderRowBehindStartRow() throws InvalidConfigurationException {
createBubbleWindow(headerRowSpinner, BubbleStyle.ERROR,
"io.dataimport.step.csv.format_specification.invalid_header_row", getHeaderRowIndex() + 1,
getSelection().getRowIndexStart() + 1);
}
/**
* Shows a "header row not found"-bubble
*/
void notifyHeaderRowNotFound() {
createBubbleWindow(headerRowSpinner, BubbleStyle.ERROR,
"io.dataimport.step.csv.format_specification.header_row_not_found");
}
/**
* Shows a central label displaying a notification to the user (e.g. for errors or during
* loading).
*
* @param i18nKey
* the notification I18N key to lookup the label text and icon
* @param arguments
* the I18N arguments
*/
private void showNotificationLabel(String i18nKey, Object... arguments) {
GridBagConstraints constraint = new GridBagConstraints();
constraint.fill = GridBagConstraints.BOTH;
constraint.weightx = 1.0;
constraint.weighty = 1.0;
centerPanel.removeAll();
centerPanel.add(new JPanel(), constraint);
constraint.weightx = 0.0;
constraint.weighty = 0.0;
constraint.fill = GridBagConstraints.NONE;
constraint.anchor = GridBagConstraints.CENTER;
centerPanel.add(new ResourceLabel(i18nKey, arguments), constraint);
constraint.weightx = 1.0;
constraint.weighty = 1.0;
constraint.fill = GridBagConstraints.BOTH;
centerPanel.add(new JPanel(), constraint);
centerPanel.revalidate();
centerPanel.repaint();
// disable all header actions
enableHeaderActions(false);
}
/**
* Allows to change whether all header actions should be enabled or disabled.
*
* @param enabled
* whether the header actions should be enabled
*/
private void enableHeaderActions(boolean enabled) {
sheetComboBox.setEnabled(enabled);
hasHeaderRowCheckBox.setEnabled(enabled);
headerRowSpinner.setEnabled(enabled);
applySelectionAction.setEnabled(enabled);
cellRangeTextField.setEnabled(enabled);
selectAllAction.setEnabled(enabled);
}
/**
* Updates the cell selection model with the provided new sheetIndex, headerRowIndex, and
* {@link CellRangeSelection}.
*
* @param sheetIndex
* the new sheetIndex
* @param headerRowIndex
* the new headerRowIndex
* @param selection
* the new cell range selection
*/
private void updateModel(int sheetIndex, int headerRowIndex, CellRangeSelection selection) {
sheetSelectionModel.updateModel(sheetIndex, headerRowIndex, selection);
}
/**
* @return the user selection of the range to be imported.
* @throws InvalidConfigurationException
* in case the current selection is invalid
*/
CellRangeSelection getSelection() throws InvalidConfigurationException {
CellRangeSelection selection = sheetSelectionModel.getCellRangeSelection();
if (selection == null) {
throw new InvalidConfigurationException();
}
return selection;
}
/**
* @return whether the current selected sheet is empty
*/
boolean isSheetEmpty() {
return contentTable.getModel().getColumnCount() <= 0;
}
/**
* @return whether the current selection is empty
*/
boolean isSelectionEmpty() {
return cellRangeTextField.getText().trim().isEmpty();
}
/**
* @return wether the UI of the sheet selection panel is currently updated
*/
boolean isUpdatingUI() {
return updatingUI;
}
/**
* Returns the index of the header row
*
* @return the index of the header row or {@link ResultSetAdapter#NO_HEADER_ROW} in case the
* user unchecks the {@link #hasHeaderRowCheckBox}.
*/
int getHeaderRowIndex() {
return sheetSelectionModel.getHeaderRowIndex();
}
/**
* Returns the selected sheet index
*
* @return the index of the selected sheet
*/
public int getSheetIndex() {
return sheetSelectionModel.getSheetIndex();
}
/**
* Clears the model cache.
*/
void clearCache() {
sheetSelectionModel.clearTableModelCache();
}
/**
* Registers a new change listener.
*
* @param changeListener
* the listener to register
*/
void addChangeListener(ChangeListener changeListener) {
this.changeListeners.add(changeListener);
}
/**
* Fires a {@link ChangeEvent} that informs the listeners of a changed state.
*/
private void fireStateChanged() {
ChangeEvent event = new ChangeEvent(this);
for (ChangeListener listener : changeListeners) {
try {
listener.stateChanged(event);
} catch (RuntimeException rte) {
LogService.getRoot().log(Level.WARNING,
"com.rapidminer.gui.io.dataimport.AbstractWizardStep.changelistener_failed", rte);
}
}
}
}