/**
* 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;
import java.awt.CardLayout;
import java.awt.Dimension;
import java.awt.GraphicsConfiguration;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.LinkedList;
import java.util.List;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.rapidminer.core.io.data.DataSetException;
import com.rapidminer.core.io.data.source.DataSource;
import com.rapidminer.core.io.data.source.DataSourceFactory;
import com.rapidminer.core.io.data.source.DataSourceFactoryRegistry;
import com.rapidminer.core.io.gui.ImportWizard;
import com.rapidminer.core.io.gui.InvalidConfigurationException;
import com.rapidminer.core.io.gui.WizardDirection;
import com.rapidminer.core.io.gui.WizardStep;
import com.rapidminer.core.io.gui.WizardStep.ButtonState;
import com.rapidminer.gui.tools.ResourceAction;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.gui.tools.dialogs.ButtonDialog;
import com.rapidminer.tools.I18N;
/**
* The {@link DataImportWizard} is the wizard dialog shown to import any kind of data into
* RapidMiner Studio. Use the {@link DataImportWizardBuilder} to construct a new instance.
*
* @author Nils Woehler
* @since 7.0.0
*
*/
final class DataImportWizard extends ButtonDialog implements ImportWizard {
private static final long serialVersionUID = 1L;
/** A loading icon */
private static final ImageIcon LOADING_ICON = SwingTools.createIcon("16/loading.gif");
/**
* A template for the header which includes a HTML progress bar.
*/
private final String INFO_LABEL_TEXT_TEMPLATE = "<div style='text-align:center;'><h2>%s</h2>"
+ "<div style='background-color: #BBBBBB; width: 100%%;height: 4px;'><div style='width: %s; background-color: #34AD65;height: 4px;'>"
+ "</div></div>";
private int progress = 0;
private final JButton previousButton = new JButton(new ResourceAction("previous") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
previousStep();
}
});
private final JButton nextButton = new JButton(new ResourceAction("next") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
nextStep();
}
});
private final Action finishAction = new ResourceAction("finish") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
// only execute the action in case the finish button is visible and enabled
if (finishButton.isVisible() && finishButton.isEnabled()) {
disableButtons();
new Thread(new Runnable() {
@Override
public void run() {
try {
getCurrentStep().viewWillBecomeInvisible(WizardDirection.NEXT);
SwingTools.invokeLater(new Runnable() {
@Override
public void run() {
accept(true);
}
});
} catch (InvalidConfigurationException e) {
updateButtons();
}
}
}).start();
}
}
};
private final JButton finishButton = new JButton(finishAction);
private final JButton cancelButton;
/**
* Listens for changes and updates the button panel and info header.
*/
private final ChangeListener stepChangeListener = new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
updateButtons();
updateInfoHeader();
}
};
private final JPanel cardPanel;
private final CardLayout cardLayout;
private final List<WizardStep> steps;
private final Icon previousIcon;
private final Icon nextIcon;
private DataSource dataSource;
private String currentStepID;
private List<String> previousStepIDs;
/**
* Constructs a new instance of the {@link DataImportWizard}.
*
* @param owner
* the dialog owner
* @param modalityType
* the modality type
* @param graphicsConfig
* the graphics config. Might be <code>null</code> if no special config should be
* used.
*/
DataImportWizard(Window owner, ModalityType modalityType, GraphicsConfiguration graphicsConfig) {
super(owner, "io.dataimport.import_wizard", modalityType, graphicsConfig);
this.cardLayout = new CardLayout();
this.cardPanel = new JPanel(cardLayout);
this.steps = new LinkedList<>();
this.previousStepIDs = new LinkedList<>();
this.cancelButton = makeCancelButton();
this.previousIcon = previousButton.getIcon();
this.nextIcon = nextButton.getIcon();
setResizable(false);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
if (wasConfirmed()) {
DataSourceFactory<?> factory = DataSourceFactoryRegistry.INSTANCE.lookUp(getDataSource().getClass());
DataImportWizardUtils.logStats(DataWizardEventType.CLOSED, "finished - " + factory.getI18NKey());
} else {
if (getDataSource() != null) {
DataSourceFactory<?> factory = DataSourceFactoryRegistry.INSTANCE.lookUp(getDataSource().getClass());
DataImportWizardUtils.logStats(DataWizardEventType.CLOSED, "aborted - " + factory.getI18NKey());
} else {
DataImportWizardUtils.logStats(DataWizardEventType.CLOSED, "aborted - no datasource");
}
}
closeDataSource();
}
});
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false),
"FINISH");
getRootPane().getActionMap().put("FINISH", finishAction);
}
/**
* Updates the dialog button, title, header and shows the {@link ImportWizardStep} referenced by
* the provided stepId.
*/
void showStep(final String stepId, WizardDirection direction) {
// log step change
switch (direction) {
case NEXT:
DataImportWizardUtils.logStats(DataWizardEventType.NEXT_STEP, stepId);
break;
case PREVIOUS:
DataImportWizardUtils.logStats(DataWizardEventType.PREVIOUS_STEP, stepId);
break;
case STARTING:
DataImportWizardUtils.logStats(DataWizardEventType.STARTING, stepId);
break;
default:
// ignore
break;
}
// lookup step
WizardStep importWizardStep = getStep(stepId);
// and notify the step that it will become visible now
try {
importWizardStep.viewWillBecomeVisible(direction);
} catch (InvalidConfigurationException e) {
return;
}
// update current and previous step ID
if (currentStepID != null && direction != WizardDirection.PREVIOUS) {
this.previousStepIDs.add(currentStepID);
}
this.currentStepID = stepId;
updateButtons();
updateTitle();
updateInfoHeader();
SwingTools.invokeLater(new Runnable() {
@Override
public void run() {
// show step
cardLayout.show(cardPanel, stepId);
}
});
}
private void updateTitle() {
setTitle(getDialogTitle() + " - " + getStepTitle());
}
private void updateInfoHeader() {
if (getCurrentStep() == null) {
return;
}
if (infoTextLabel != null) {
SwingTools.disableClearType(infoTextLabel);
infoTextLabel.setText(String.format(INFO_LABEL_TEXT_TEMPLATE, getStepTitle(), progress + "%"));
}
try {
getCurrentStep().validate();
} catch (InvalidConfigurationException e) {
// ignore
}
}
private String getStepTitle() {
return I18N.getGUIMessage("gui.dialog.io.dataimport.step." + getCurrentStep().getI18NKey() + ".title");
}
/**
* Updates the button status within the EDT by calling {@link SwingTools#invokeLater(Runnable)}.
*/
private void updateButtons() {
SwingTools.invokeLater(new Runnable() {
@Override
public void run() {
// adapt previous button
WizardStep currentStep = getCurrentStep();
if (currentStep == null) {
return;
}
previousButton.setIcon(previousIcon);
previousButton.setEnabled(
currentStep.getPreviousButtonState() == ButtonState.ENABLED && !previousStepIDs.isEmpty());
previousButton.setVisible(currentStep.getPreviousButtonState() != ButtonState.HIDDEN);
// adapt next and finish buttons
nextButton.setIcon(nextIcon);
nextButton.setEnabled(currentStep.getNextButtonState() == ButtonState.ENABLED);
boolean isLastStep = isLastStep(currentStep);
nextButton.setVisible(!isLastStep && currentStep.getNextButtonState() != ButtonState.HIDDEN);
finishButton.setEnabled(currentStep.getNextButtonState() == ButtonState.ENABLED);
finishButton.setVisible(isLastStep && currentStep.getNextButtonState() != ButtonState.HIDDEN);
cancelButton.setEnabled(true);
}
});
}
private void disableButtons() {
SwingTools.invokeLater(new Runnable() {
@Override
public void run() {
previousButton.setEnabled(false);
nextButton.setEnabled(false);
finishButton.setEnabled(false);
cancelButton.setEnabled(false);
}
});
}
/**
* @param importWizardStep
* @return
*/
private boolean isLastStep(WizardStep importWizardStep) {
return importWizardStep != null && importWizardStep.getNextStepID() == null;
}
/**
* Looks up the {@link ImportWizardStep} for the provided step ID.
*
* @param stepId
* the step ID used to lookup the requested {@link ImportWizardStep}
* @return either the {@link ImportWizardStep} for the provided ID or <code>null</code> if no
* step could be found
*/
private WizardStep getStep(String stepId) {
for (WizardStep step : steps) {
if (step.getI18NKey().equals(stepId)) {
return step;
}
}
return null;
}
/**
* @return the current step. Might return {@code null} in case there is no step yet.
*/
private WizardStep getCurrentStep() {
return getStep(currentStepID);
}
/**
* Layouts the dialog by adding the dialog main content and updating the current shown step.
* Should only be called once after the dialog instance has been created.
*
* @param size
* the dialog size.
*/
void layoutDefault(int size, String startingStepId) {
super.layoutDefault(cardPanel, size, previousButton, nextButton, finishButton, cancelButton);
// fix button size such that it is not affected by icon change
final Dimension nextSize = new Dimension(nextButton.getWidth(), nextButton.getHeight());
nextButton.setMinimumSize(nextSize);
nextButton.setPreferredSize(nextSize);
final Dimension previousSize = new Dimension(previousButton.getWidth(), previousButton.getHeight());
previousButton.setMinimumSize(previousSize);
previousButton.setPreferredSize(previousSize);
showStep(startingStepId, WizardDirection.STARTING);
}
@Override
public void addStep(WizardStep newStep) {
// check if step with same key was registered before and remove if true
WizardStep oldStep = getStep(newStep.getI18NKey());
if (oldStep != null) {
removeStep(oldStep);
}
// add new step
this.steps.add(newStep);
this.cardPanel.add(newStep.getView(), newStep.getI18NKey());
newStep.addChangeListener(stepChangeListener);
}
private void removeStep(WizardStep step) {
step.removeChangeListener(stepChangeListener);
this.steps.remove(step);
this.cardPanel.remove(step.getView());
}
@Override
public void previousStep() {
previousButton.setIcon(LOADING_ICON);
disableButtons();
new Thread(new Runnable() {
@Override
public void run() {
try {
getCurrentStep().viewWillBecomeInvisible(WizardDirection.PREVIOUS);
// remove step ID from list and show step
String previousStepID = previousStepIDs.remove(previousStepIDs.size() - 1);
showStep(previousStepID, WizardDirection.PREVIOUS);
} catch (InvalidConfigurationException e) {
updateButtons();
}
}
}).start();
}
@Override
public void nextStep(final String stepId) {
nextButton.setIcon(LOADING_ICON);
disableButtons();
new Thread(new Runnable() {
@Override
public void run() {
try {
getCurrentStep().viewWillBecomeInvisible(WizardDirection.NEXT);
showStep(stepId, WizardDirection.NEXT);
} catch (InvalidConfigurationException e) {
updateButtons();
}
}
}).start();
}
@Override
public void nextStep() {
nextButton.setIcon(LOADING_ICON);
disableButtons();
new Thread(new Runnable() {
@Override
public void run() {
try {
/*
* Implementation almost equal to nextStep(String) but we need to call
* viewWillBecomeInvisible() before calling getNextStepID() so we cannot call
* nextStep(String) here.
*/
getCurrentStep().viewWillBecomeInvisible(WizardDirection.NEXT);
String nextStepID = getCurrentStep().getNextStepID();
showStep(nextStepID, WizardDirection.NEXT);
} catch (InvalidConfigurationException e) {
updateButtons();
}
}
}).start();
}
@Override
public <D extends DataSource> void setDataSource(final D dataSource, final DataSourceFactory<D> factory) {
// close data source if data source was already specified
closeDataSource();
// update the data source
setDataSource(dataSource);
// log data source selection
DataImportWizardUtils.logStats(DataWizardEventType.DATASOURCE_SELECTED, factory.getI18NKey());
// add data source custom steps right after the current steps but before the concluding
// steps
SwingTools.invokeAndWait(new Runnable() {
@Override
public void run() {
List<WizardStep> customSteps = factory.createCustomSteps(DataImportWizard.this, dataSource);
for (WizardStep step : customSteps) {
addStep(step);
}
}
});
}
private void closeDataSource() {
if (getDataSource() != null) {
try {
getDataSource().close();
} catch (DataSetException e) {
// ignore, can't do anything here anyway
}
}
}
@Override
public <D> D getDataSource(Class<? extends D> dsClass) throws InvalidConfigurationException {
DataSource ds = getDataSource();
if (ds == null) {
return null;
} else if (dsClass.isAssignableFrom(ds.getClass())) {
return dsClass.cast(ds);
} else {
throw new InvalidConfigurationException();
}
}
private DataSource getDataSource() {
return dataSource;
}
private void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void setProgress(int progress) {
this.progress = Math.min(Math.max(progress, 0), 100);
}
@Override
public JDialog getDialog() {
return this;
}
}