/** * 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.celleditors.value; import java.awt.Component; import java.awt.Container; 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.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.lang.reflect.InvocationTargetException; import java.util.List; import javax.swing.AbstractCellEditor; import javax.swing.DefaultComboBoxModel; import javax.swing.DefaultListCellRenderer; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.plaf.basic.BasicComboPopup; import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.autocomplete.AutoCompleteComboBoxAddition; import com.rapidminer.operator.Operator; import com.rapidminer.parameter.ParameterType; import com.rapidminer.tools.I18N; import com.rapidminer.tools.ProgressListener; import com.rapidminer.tools.config.Configurable; /** * Renders a combo box which can be filled with suggestions. * * @author Marcin Skirzynski, Nils Woehler */ public abstract class AbstractSuggestionBoxValueCellEditor extends AbstractCellEditor implements PropertyValueCellEditor { private static final long serialVersionUID = -771727412083431607L; /** * The model of the combo box which consist of the suggestions */ private final SuggestionComboBoxModel model; /** * The GUI element */ private final SuggestionComboBox comboBox; private final JPanel container; private Operator operator; private ParameterType type; private final static Object LOADING = new Object(); private final static Object EMPTY = new Object(); private final static String LOADING_STRING = I18N.getGUILabel("parameters.loading"); private final static String EMPTY_STRING = I18N.getGUILabel("parameters.empty"); private ActionListener comboBoxSelectionListener = new ActionListener() { private Object currentItem = null; @Override public void actionPerformed(ActionEvent e) { Object selectedItem = comboBox.getSelectedItem(); if (EMPTY.equals(selectedItem) || LOADING.equals(selectedItem)) { if (currentItem == null) { comboBox.setSelectedItem(""); } else { comboBox.setSelectedItem(currentItem); } } else { currentItem = selectedItem; } } }; public AbstractSuggestionBoxValueCellEditor(final ParameterType type) { this.type = type; this.model = new SuggestionComboBoxModel(); this.comboBox = new SuggestionComboBox(model); this.comboBox.setToolTipText(type.getDescription()); this.comboBox.setRenderer(new SuggestionComboBoxModelCellRenderer()); comboBox.addActionListener(comboBoxSelectionListener); new AutoCompleteComboBoxAddition(comboBox); this.container = new JPanel(new GridBagLayout()); this.container.setToolTipText(type.getDescription()); GridBagConstraints c = new GridBagConstraints(); c.fill = GridBagConstraints.BOTH; c.weighty = 1; c.weightx = 1; container.add(comboBox, c); } /** * @return the parameter type of the suggestion box */ protected ParameterType getParameterType() { return type; } /** * @return the combo box ui element displaying the selection */ protected SuggestionComboBox getSuggestionComboBox() { return comboBox; } /** * Classes extending the {@link AbstractSuggestionBoxValueCellEditor} have to provide * suggestions via this method. It is called whenever the combobox if opened. * * @param operator * the operator which is being configured. <b>CAUTION</b>: Can be <code>null</code> * if parameter type is used to e.g. configure {@link Configurable}s. * @param progressListener * the progress listener to report the progress to */ public abstract List<Object> getSuggestions(Operator operator, ProgressListener progressListener); /** * @return the current value. If there is an operator, the operator is asked, otherwise the * value from the combobox is taken. */ private String getValue() { if (operator == null) { return String.valueOf(comboBox.getEditor().getItem()); } return operator.getParameters().getParameterOrNull(type.getKey()); } @Override public boolean rendersLabel() { return false; } @Override public boolean useEditorAsRenderer() { return true; } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { comboBox.setSelectedItem(value); return container; } @Override public Object getCellEditorValue() { return comboBox.getEditor().getItem(); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { comboBox.setSelectedItem(value); return container; } @Override public void setOperator(Operator operator) { this.operator = operator; } public class SuggestionComboBoxModelCellRenderer extends DefaultListCellRenderer { private static final long serialVersionUID = 1L; @Override public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { Component listCellRendererComponent = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); if (LOADING.equals(value) || EMPTY.equals(value)) { listCellRendererComponent.setBackground(list.getBackground()); listCellRendererComponent.setForeground(UIManager.getColor("Label.disabledForeground")); listCellRendererComponent.setEnabled(false); super.setText(LOADING == value ? LOADING_STRING : EMPTY_STRING); } return listCellRendererComponent; } } class SuggestionComboBoxModel extends DefaultComboBoxModel<Object> { private static final long serialVersionUID = -2984664300141879731L; private Object lock = new Object(); public void updateModel() { fireEditingStopped(); final Object selected = getValue(); ProgressThread t = new ProgressThread("fetching_suggestions") { @Override public void run() { try { getProgressListener().setTotal(100); getProgressListener().setCompleted(0); synchronized (lock) { removeAllElements(); insertElementAt(LOADING, 0); comboBox.getEditor().setItem(selected); // fill list with stuff final List<Object> suggestions = getSuggestions(operator, getProgressListener()); removeAllElements(); int index = 0; for (Object suggestion : suggestions) { insertElementAt(suggestion, index); ++index; } if (suggestions.size() == 0) { insertElementAt(EMPTY, 0); } // resize popup try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { Object child = comboBox.getAccessibleContext().getAccessibleChild(0); BasicComboPopup popup = (BasicComboPopup) child; JList<?> list = popup.getList(); Dimension preferred = list.getPreferredSize(); preferred.width = container.getWidth() - 3; int itemCount = comboBox.getItemCount(); int rowHeight = 10; if (itemCount > 0) { rowHeight = preferred.height / itemCount; } int maxHeight = comboBox.getMaximumRowCount() * rowHeight; preferred.height = Math.min(preferred.height, maxHeight); Container c = SwingUtilities.getAncestorOfClass(JScrollPane.class, list); JScrollPane scrollPane = (JScrollPane) c; scrollPane.setPreferredSize(preferred); scrollPane.setMaximumSize(preferred); Dimension popupSize = popup.getSize(); popupSize.width = preferred.width; popupSize.height = preferred.height + 5; Component parent = popup.getParent(); if (parent != null) { parent.setSize(popupSize); parent.validate(); parent.repaint(); } } }); comboBox.getEditor().setItem(selected); } catch (InvocationTargetException e) { // do nothing } catch (InterruptedException e) { // do nothing } } getProgressListener().setCompleted(100); } finally { getProgressListener().complete(); } } }; t.addDependency("fetching_suggestions"); t.start(); } } class SuggestionComboBox extends JComboBox<Object> { private static final long serialVersionUID = 4000279412600950101L; private SuggestionComboBox(final SuggestionComboBoxModel model) { super(model); setEditable(true); addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { fireEditingStopped(); } } }); getEditor().getEditorComponent().addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { if (!e.isTemporary()) { fireEditingStopped(); } } @Override public void focusGained(FocusEvent e) {} }); // add popup menu listener Object child = getAccessibleContext().getAccessibleChild(0); BasicComboPopup popup = (BasicComboPopup) child; popup.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { model.updateModel(); } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {} @Override public void popupMenuCanceled(PopupMenuEvent e) {} }); } } /** * @param button * adds a button the the right side of the ComboBox. */ protected void addConfigureButton(JButton button) { GridBagConstraints c = new GridBagConstraints(); c.fill = GridBagConstraints.BOTH; c.weighty = 1; c.weightx = 0; c.insets = new Insets(0, 5, 0, 0); container.add(button, c); } }