/**
* 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;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import org.jdesktop.swingx.JXTextField;
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.gui.tools.ResourceAction;
import com.rapidminer.gui.tools.ResourceLabel;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.gui.tools.components.LinkRemoteButton;
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.update.internal.UpdateManagerRegistry;
/**
* The type selection view which is shown within the {@link ImportWizard} to allow the selection of
* the {@link DataSource} type.
*
* @author Nils Woehler
* @since 7.0.0
*/
public final class TypeSelectionView extends JPanel {
private static final Dimension SCROLL_PANE_SIZE = new Dimension(700, 400);
private static final Dimension SEARCH_FIELD_SIZE = new Dimension(550, 40);
private static final Dimension TYPE_BUTTON_DIMENSION = new Dimension(280, 60);
/** re-use same log timer for all data import wizard dialogs */
private static final Timer SEARCH_ACTION_LOG_TIMER = new Timer(500, null);
private final ResourceAction searchInMarketplaceAction = new ResourceAction(true,
"io.dataimport.step.type_selection.search_in_mp") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
openMarketplaceDialog();
}
};
private final ResourceAction tryMarketplaceSearchAction = new ResourceAction(true,
"io.dataimport.step.type_selection.try_searching_marketplace") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
openMarketplaceDialog();
}
};
// Disable search text field until there are more data sources available
// (just uncomment code below in case it should be added again)
// private final ResourceAction CLEAR_SEARCH_ACTION = new ResourceAction(true, "clear_filter") {
//
// private static final long serialVersionUID = 1L;
//
// @Override
// public void actionPerformed(final ActionEvent e) {
// searchTextField.setText(null);
// searchTextField.requestFocusInWindow();
// }
// };
// private final ImageIcon CLEAR_SEARCH_HOVERED_ICON =
// SwingTools.createIcon("16/x-mark_orange.png");
private static final long serialVersionUID = 1L;
private final JXTextField searchTextField;
private final ImportWizard wizard;
private final JPanel mainContentPanel;
private final List<JButton> dataSourceSelectionButtons = new ArrayList<>();
/**
* Constructs a new type selection view instance.
*
* @param wizard
* the data import wizard
*/
public TypeSelectionView(final ImportWizard wizard) {
this.wizard = wizard;
JPanel dataSourcesPanel = new JPanel(new GridBagLayout());
GridBagConstraints constraint = new GridBagConstraints();
// Search field
{
this.searchTextField = new JXTextField(I18N.getGUILabel("io.dataimport.step.type_selection.search_placeholder"));
this.searchTextField.getDocument().addUndoableEditListener(new UndoableEditListener() {
@Override
public void undoableEditHappened(UndoableEditEvent e) {
String searchTerm = searchTextField.getText();
if (searchTerm != null && !searchTerm.trim().isEmpty() && searchTerm.trim().length() > 1) {
updateTypeSelectionContentPanel(searchTerm, false);
} else {
updateTypeSelectionContentPanel(null, false);
}
}
});
searchTextField.setMinimumSize(SEARCH_FIELD_SIZE);
searchTextField.setPreferredSize(SEARCH_FIELD_SIZE);
// Disable search text field until there are more data sources available
// (just uncomment code below in case it should be added again)
// TextFieldWithAction outerTextField = new TextFieldWithAction(searchTextField,
// CLEAR_SEARCH_ACTION,
// CLEAR_SEARCH_HOVERED_ICON);
//
// constraint.weightx = 1;
// constraint.weighty = 0;
// constraint.gridwidth = GridBagConstraints.REMAINDER;
// constraint.fill = GridBagConstraints.VERTICAL;
// constraint.insets = new Insets(30, 0, 20, 0);
// dataSourcesPanel.add(outerTextField, constraint);
}
// Center panel
{
JPanel centerPanel = new JPanel(new GridBagLayout());
GridBagConstraints centerConstraint = new GridBagConstraints();
centerConstraint.fill = GridBagConstraints.BOTH;
centerConstraint.weightx = 1;
centerConstraint.gridy = 0;
JPanel leftFillPanel = new JPanel();
centerPanel.add(leftFillPanel, centerConstraint);
centerConstraint.fill = GridBagConstraints.BOTH;
centerConstraint.weightx = 0;
centerConstraint.insets = new Insets(20, 0, 0, 0);
this.mainContentPanel = new JPanel(new GridBagLayout());
centerPanel.add(mainContentPanel, centerConstraint);
centerConstraint.fill = GridBagConstraints.BOTH;
centerConstraint.weightx = 1;
centerConstraint.insets = new Insets(0, 0, 0, 0);
JPanel rightFillPanel = new JPanel();
centerPanel.add(rightFillPanel, centerConstraint);
centerConstraint.fill = GridBagConstraints.BOTH;
centerConstraint.gridy += 1;
centerConstraint.weightx = 1;
centerConstraint.weighty = 1;
centerConstraint.gridwidth = GridBagConstraints.REMAINDER;
JPanel bottomFillPanel = new JPanel();
centerPanel.add(bottomFillPanel, centerConstraint);
constraint.weighty = 1;
constraint.gridwidth = GridBagConstraints.REMAINDER;
constraint.fill = GridBagConstraints.BOTH;
constraint.insets = new Insets(0, 0, 0, 0);
dataSourcesPanel.add(centerPanel, constraint);
}
add(dataSourcesPanel);
updateTypeSelectionContentPanel(null, true);
}
private JButton createDataSourceSelectionButton(@SuppressWarnings("rawtypes") final DataSourceFactory factory) {
String label = DataImportWizardUtils.getFactoryLabel(factory);
String description = DataImportWizardUtils.getFactoryDescription(factory);
JButton typeSelectionButton = new JButton(new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
@SuppressWarnings("unchecked")
public void actionPerformed(ActionEvent e) {
enableDataSourceButtons(false);
// update the wizard by setting the selected factory
wizard.setDataSource(factory.createNew(), factory);
// switch to the next wizard step (location selection)
wizard.nextStep();
}
});
typeSelectionButton.setText(label);
typeSelectionButton.setToolTipText(description);
typeSelectionButton.setMinimumSize(TYPE_BUTTON_DIMENSION);
typeSelectionButton.setPreferredSize(TYPE_BUTTON_DIMENSION);
typeSelectionButton.setMaximumSize(TYPE_BUTTON_DIMENSION);
typeSelectionButton.setIcon(DataImportWizardUtils.getFactoryIcon(factory));
return typeSelectionButton;
}
private void enableDataSourceButtons(boolean enable) {
for (JButton button : dataSourceSelectionButtons) {
button.setEnabled(enable);
}
}
@SuppressWarnings("rawtypes")
private List<DataSourceFactory> getFilteredFactories(String searchTerm) {
List<DataSourceFactory<?>> factories = new LinkedList<>(DataSourceFactoryRegistry.INSTANCE.getFactories());
List<DataSourceFactory> result = new LinkedList<>();
if (searchTerm == null || searchTerm.trim().isEmpty()) {
for (DataSourceFactory factory : factories) {
result.add(factory);
}
} else {
// filter factories according to search term
for (DataSourceFactory factory : factories) {
String label = DataImportWizardUtils.getFactoryLabel(factory);
String description = DataImportWizardUtils.getFactoryDescription(factory);
boolean labelMatches = label.toLowerCase(Locale.ENGLISH).contains(searchTerm.toLowerCase(Locale.ENGLISH));
boolean descriptionMatches = description.toLowerCase(Locale.ENGLISH)
.contains(searchTerm.toLowerCase(Locale.ENGLISH));
boolean matchesSearchTerm = labelMatches || descriptionMatches;
if (matchesSearchTerm) {
result.add(factory);
}
}
}
return result;
}
private void updateTypeSelectionContentPanel(final String searchTerm, final boolean requestFocusForButton) {
// stop timer
SEARCH_ACTION_LOG_TIMER.stop();
// schedule new task to log search term
if (searchTerm != null) {
// remove all action listeners
for (ActionListener l : SEARCH_ACTION_LOG_TIMER.getActionListeners()) {
SEARCH_ACTION_LOG_TIMER.removeActionListener(l);
}
// add new action listener
SEARCH_ACTION_LOG_TIMER.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
DataImportWizardUtils.logStats(DataWizardEventType.SEARCH_TYPE, searchTerm);
}
});
// start countdown
SEARCH_ACTION_LOG_TIMER.setRepeats(false);
SEARCH_ACTION_LOG_TIMER.start();
}
SwingTools.invokeLater(new Runnable() {
@Override
@SuppressWarnings("rawtypes")
public void run() {
JButton focusButton = null;
boolean resultEmpty = false;
// clear panel
mainContentPanel.removeAll();
// add factory buttons panel
{
JPanel factoryButtonPanel = new JPanel(new GridBagLayout());
JScrollPane scrollPane = new JScrollPane(factoryButtonPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setMaximumSize(SCROLL_PANE_SIZE);
scrollPane.setBorder(BorderFactory.createEmptyBorder());
GridBagConstraints constraint = new GridBagConstraints();
constraint.gridx = 0;
constraint.gridy = 0;
constraint.insets = new Insets(5, 0, 5, 15);
constraint.fill = GridBagConstraints.NONE;
// retrieve factories
List<DataSourceFactory> factoryMatches = getFilteredFactories(searchTerm);
// reverse list so first registered factories are displayed at the beginning
Collections.reverse(factoryMatches);
resultEmpty = factoryMatches.isEmpty();
// set preferred size for scroll pane in case of more than 10 factories.
// Otherwise the scrollbar won't show up.
if (factoryMatches.size() > 10) {
scrollPane.setPreferredSize(SCROLL_PANE_SIZE);
}
dataSourceSelectionButtons.clear();
// show a type selection button for each data source factory
for (DataSourceFactory factory : factoryMatches) {
JButton typeSelectionButton = createDataSourceSelectionButton(factory);
dataSourceSelectionButtons.add(typeSelectionButton);
if (focusButton == null) {
focusButton = typeSelectionButton;
}
factoryButtonPanel.add(typeSelectionButton, constraint);
// update constraints for next button
constraint.gridx += 1;
constraint.insets = new Insets(5, 0, 5, 0);
if (constraint.gridx > 1) {
constraint.gridx = 0;
constraint.gridy += 1;
constraint.insets = new Insets(5, 0, 5, 15);
}
}
// fix for uneven number of data sources
if (factoryMatches.size() % 2 == 1) {
constraint.gridx = 0;
constraint.gridy += 1;
constraint.insets = new Insets(5, 0, 5, 15);
}
constraint = new GridBagConstraints();
constraint.fill = GridBagConstraints.NONE;
constraint.gridwidth = GridBagConstraints.REMAINDER;
constraint.weightx = 1;
constraint.weighty = 0;
mainContentPanel.add(scrollPane, constraint);
}
// add link button below factory buttons
{
JPanel linkButtonPanel = new JPanel();
// Add "empty result" text in case the search was empty
if (resultEmpty) {
JPanel noResultsPanel = new JPanel(new BorderLayout());
JLabel noResultsSymbol = new ResourceLabel("io.dataimport.step.type_selection.empty_search_symbol");
noResultsSymbol.setHorizontalAlignment(SwingConstants.CENTER);
noResultsSymbol.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
noResultsPanel.add(noResultsSymbol, BorderLayout.NORTH);
JLabel noResultsLabel = new ResourceLabel("io.dataimport.step.type_selection.empty_search",
searchTerm);
noResultsPanel.add(noResultsLabel, BorderLayout.CENTER);
JPanel searchInMpPanel = new JPanel();
LinkRemoteButton searchInMarketplace = new LinkRemoteButton(tryMarketplaceSearchAction);
searchInMarketplace.setAlignmentX(SwingConstants.CENTER);
searchInMpPanel.add(searchInMarketplace);
noResultsPanel.add(searchInMpPanel, BorderLayout.SOUTH);
linkButtonPanel.add(noResultsPanel);
} else {
// add "search marketplace" link button
LinkRemoteButton searchInMarketplace = new LinkRemoteButton(searchInMarketplaceAction);
searchInMarketplace.setAlignmentX(SwingConstants.CENTER);
linkButtonPanel.add(searchInMarketplace);
}
GridBagConstraints constraint = new GridBagConstraints();
constraint.insets = new Insets(10, 0, 0, 0);
constraint.gridwidth = GridBagConstraints.REMAINDER;
constraint.fill = GridBagConstraints.BOTH;
constraint.weighty = 1;
mainContentPanel.add(linkButtonPanel, constraint);
}
mainContentPanel.revalidate();
mainContentPanel.repaint();
if (requestFocusForButton && focusButton != null) {
final JButton requestFocusButton = focusButton;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
requestFocusButton.requestFocusInWindow();
}
});
}
}
});
}
private void openMarketplaceDialog() {
try {
UpdateManagerRegistry.INSTANCE.get().showUpdateDialog(false);
updateTypeSelectionContentPanel(null, false);
} catch (URISyntaxException | IOException e1) {
SwingTools.showSimpleErrorMessage("io.dataimport.step.type_selection.marketplace_connection_error", e1);
}
}
/**
* Ensures that the data source buttons are enabled.
*/
void enableButtons() {
enableDataSourceButtons(true);
}
}