/*
* This file is part of ELKI:
* Environment for Developing KDD-Applications Supported by Index-Structures
*
* Copyright (C) 2017
* ELKI Development Team
*
* 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 de.lmu.ifi.dbs.elki.gui.util;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.FileDialog;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import javax.swing.AbstractCellEditor;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.DefaultCellEditor;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableColumn;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import de.lmu.ifi.dbs.elki.gui.icons.StockIcon;
import de.lmu.ifi.dbs.elki.gui.util.ClassTree.ClassNode;
import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ClassListParameter;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ClassParameter;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.EnumParameter;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.FileParameter;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Parameter;
/**
* Class showing a table of ELKI parameters.
*
* @author Erich Schubert
* @since 0.3
*
* @apiviz.composedOf ParametersModel
* @apiviz.owns de.lmu.ifi.dbs.elki.gui.util.ParameterTable.ColorfulRenderer
* @apiviz.owns de.lmu.ifi.dbs.elki.gui.util.ParameterTable.DropdownEditor
* @apiviz.owns de.lmu.ifi.dbs.elki.gui.util.ParameterTable.FileNameEditor
* @apiviz.owns de.lmu.ifi.dbs.elki.gui.util.ParameterTable.ClassListEditor
* @apiviz.owns de.lmu.ifi.dbs.elki.gui.util.ParameterTable.AdjustingEditor
*/
public class ParameterTable extends JTable {
/**
* Serial version
*/
private static final long serialVersionUID = 1L;
/**
* Color for parameters that are not optional and not yet specified.
*/
static final Color COLOR_INCOMPLETE = new Color(0xFFCF9F);
/**
* Color for parameters with an invalid value.
*/
static final Color COLOR_SYNTAX_ERROR = new Color(0xFFAFAF);
/**
* Color for optional parameters (with no default value)
*/
static final Color COLOR_OPTIONAL = new Color(0xDFFFDF);
/**
* Color for parameters having a default value.
*/
static final Color COLOR_DEFAULT_VALUE = new Color(0xDFDFDF);
/**
* Containing frame.
*/
protected Frame frame;
/**
* The parameters we edit.
*/
protected DynamicParameters parameters;
/**
* Constructor
*
* @param frame Containing frame
* @param pm Parameter Model
* @param parameters Parameter storage
*/
public ParameterTable(Frame frame, ParametersModel pm, DynamicParameters parameters) {
super(pm);
this.frame = frame;
this.parameters = parameters;
this.setFillsViewportHeight(true);
final ColorfulRenderer colorfulRenderer = new ColorfulRenderer();
this.setDefaultRenderer(Parameter.class, colorfulRenderer);
this.setDefaultRenderer(String.class, colorfulRenderer);
final AdjustingEditor editor = new AdjustingEditor();
this.setDefaultEditor(String.class, editor);
this.setAutoResizeMode(AUTO_RESIZE_ALL_COLUMNS);
TableColumn col1 = this.getColumnModel().getColumn(0);
final int ppi = java.awt.Toolkit.getDefaultToolkit().getScreenResolution();
col1.setPreferredWidth(2 * ppi);
TableColumn col2 = this.getColumnModel().getColumn(1);
col2.setPreferredWidth(8 * ppi);
this.addKeyListener(new Handler());
// Increase row height, to make editors usable.
// FIXME: Heuristic hack. Any way to make this more reasonable?
setRowHeight(getRowHeight() + (int) (ppi * 0.05));
}
/**
* Internal key listener.
*
* @author Erich Schubert
*
* @apiviz.exclude
*/
protected class Handler implements KeyListener {
@Override
public void keyTyped(KeyEvent e) {
// ignore
}
@Override
public void keyPressed(KeyEvent e) {
if((e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) {
if(e.getKeyCode() == KeyEvent.VK_SPACE //
|| e.getKeyCode() == KeyEvent.VK_ENTER //
|| e.getKeyCode() == KeyEvent.VK_DOWN //
|| e.getKeyCode() == KeyEvent.VK_KP_DOWN) {
final ParameterTable parent = ParameterTable.this;
if(!parent.isEditing()) {
int leadRow = parent.getSelectionModel().getLeadSelectionIndex();
int leadColumn = parent.getColumnModel().getSelectionModel().getLeadSelectionIndex();
parent.editCellAt(leadRow, leadColumn);
Component editorComponent = getEditorComponent();
// This is a hack, to make the content assist open immediately.
if(editorComponent instanceof DispatchingPanel) {
KeyListener[] l = ((DispatchingPanel) editorComponent).component.getKeyListeners();
for(KeyListener li : l) {
li.keyPressed(e);
}
}
}
}
}
}
@Override
public void keyReleased(KeyEvent e) {
// ignore
}
}
/**
* Renderer for the table that colors the entries according to their bitmask.
*
* @author Erich Schubert
*/
private class ColorfulRenderer extends DefaultTableCellRenderer {
/**
* Serial Version
*/
private static final long serialVersionUID = 1L;
/**
* Constructor.
*/
public ColorfulRenderer() {
super();
}
@Override
public void setValue(Object value) {
if(value instanceof String) {
setText((String) value);
setToolTipText(null);
return;
}
if(value instanceof DynamicParameters.Node) {
Parameter<?> o = ((DynamicParameters.Node) value).param;
// Simulate a tree using indentation - there is no JTreeTable AFAICT
StringBuilder buf = new StringBuilder();
for(int i = 1; i < ((DynamicParameters.Node) value).depth; i++) {
buf.append(' ');
}
buf.append(o.getOptionID().getName());
setText(buf.toString());
setToolTipText(o.getOptionID().getDescription());
return;
}
setText("");
setToolTipText(null);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if(!hasFocus) {
if(row < parameters.size()) {
int flags = parameters.getNode(row).flags;
// TODO: don't hardcode black - maybe mix the other colors, too?
c.setForeground(Color.BLACK);
if((flags & DynamicParameters.BIT_INVALID) != 0) {
c.setBackground(COLOR_SYNTAX_ERROR);
}
else if((flags & DynamicParameters.BIT_SYNTAX_ERROR) != 0) {
c.setBackground(COLOR_SYNTAX_ERROR);
}
else if((flags & DynamicParameters.BIT_INCOMPLETE) != 0) {
c.setBackground(COLOR_INCOMPLETE);
}
else if((flags & DynamicParameters.BIT_DEFAULT_VALUE) != 0) {
c.setBackground(COLOR_DEFAULT_VALUE);
}
else if((flags & DynamicParameters.BIT_OPTIONAL) != 0) {
c.setBackground(COLOR_OPTIONAL);
}
else {
c.setBackground(null);
}
}
}
return c;
}
}
/**
* Editor using a Dropdown box to offer known values to choose from.
*
* @author Erich Schubert
*/
private class DropdownEditor extends DefaultCellEditor implements KeyListener {
/**
* Serial Version
*/
private static final long serialVersionUID = 1L;
/**
* We need a panel to ensure focusing.
*/
final JPanel panel;
/**
* Combo box to use
*/
private final JComboBox<String> comboBox;
/**
* Constructor.
*
* @param comboBox Combo box we're going to use
*/
public DropdownEditor(JComboBox<String> comboBox) {
super(comboBox);
this.comboBox = comboBox;
panel = new DispatchingPanel((JComponent) comboBox.getEditor().getEditorComponent());
panel.setLayout(new BorderLayout());
panel.add(comboBox, BorderLayout.CENTER);
comboBox.setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 3));
}
@Override
public void keyTyped(KeyEvent e) {
// Ignore
}
@Override
public void keyPressed(KeyEvent e) {
if((e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) {
if(e.getKeyCode() == KeyEvent.VK_SPACE //
|| e.getKeyCode() == KeyEvent.VK_ENTER //
|| e.getKeyCode() == KeyEvent.VK_DOWN //
|| e.getKeyCode() == KeyEvent.VK_KP_DOWN) {
if(!comboBox.isPopupVisible()) {
comboBox.showPopup();
e.consume();
}
}
}
}
@Override
public void keyReleased(KeyEvent e) {
// Ignore
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
// remove old contents
comboBox.removeAllItems();
// Put the current value in first.
Object val = table.getValueAt(row, column);
if(val != null && val instanceof String) {
String sval = (String) val;
if(sval.equals(DynamicParameters.STRING_OPTIONAL)) {
sval = "";
}
if(sval.startsWith(DynamicParameters.STRING_USE_DEFAULT)) {
sval = "";
}
if(sval != "") {
comboBox.addItem(sval);
comboBox.setSelectedIndex(0);
}
}
if(row < parameters.size()) {
Parameter<?> option = parameters.getNode(row).param;
// for Flag parameters.
if(option instanceof Flag) {
if(!Flag.SET.equals(val)) {
comboBox.addItem(Flag.SET);
}
if(!Flag.NOT_SET.equals(val)) {
comboBox.addItem(Flag.NOT_SET);
}
}
// and for Enum parameters.
else if(option instanceof EnumParameter<?>) {
EnumParameter<?> ep = (EnumParameter<?>) option;
for(String s : ep.getPossibleValues()) {
if(ep.hasDefaultValue() && ep.getDefaultValueAsString().equals(s)) {
comboBox.addItem(DynamicParameters.STRING_USE_DEFAULT + s);
}
else if(!s.equals(val)) {
comboBox.addItem(s);
}
}
}
// No completion for others
}
return panel;
}
}
/**
* Editor for selecting input and output file and folders names
*
* @author Erich Schubert
*/
private class FileNameEditor extends AbstractCellEditor implements TableCellEditor, ActionListener, KeyListener {
/**
* Serial version number
*/
private static final long serialVersionUID = 1L;
/**
* We need a panel to put our components on.
*/
final JPanel panel;
/**
* Text field to store the name
*/
final JTextField textfield = new JTextField();
/**
* The button to open the file selector
*/
final JButton button = new JButton("...");
/**
* File selector mode.
*/
int mode = FileDialog.LOAD;
/**
* Default path.
*/
String defaultpath = (new File(".")).getAbsolutePath();
/**
* Constructor.
*/
public FileNameEditor() {
button.addActionListener(this);
panel = new DispatchingPanel(textfield);
panel.setLayout(new BorderLayout());
panel.add(textfield, BorderLayout.CENTER);
panel.add(button, BorderLayout.EAST);
textfield.setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 3));
textfield.addKeyListener(this);
}
/**
* Button callback to show the file selector
*/
@Override
public void actionPerformed(ActionEvent e) {
FileDialog fc = new FileDialog(frame);
fc.setDirectory(defaultpath);
fc.setMode(mode);
final String curr = textfield.getText();
if(curr != null && curr.length() > 0) {
fc.setFile(curr);
}
fc.setVisible(true);
String filename = fc.getFile();
if(filename != null) {
textfield.setText(new File(fc.getDirectory(), filename).getPath());
}
fc.setVisible(false);
fc.dispose();
textfield.requestFocus();
fireEditingStopped();
}
@Override
public void keyTyped(KeyEvent e) {
// Ignore
}
@Override
public void keyPressed(KeyEvent e) {
if((e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) {
if(e.getKeyCode() == KeyEvent.VK_SPACE //
|| e.getKeyCode() == KeyEvent.VK_ENTER //
|| e.getKeyCode() == KeyEvent.VK_DOWN //
|| e.getKeyCode() == KeyEvent.VK_KP_DOWN) {
e.consume();
actionPerformed(new ActionEvent(e.getSource(), ActionEvent.ACTION_PERFORMED, "assist"));
}
}
}
@Override
public void keyReleased(KeyEvent e) {
// Ignore
}
/**
* Delegate getCellEditorValue to the text field.
*/
@Override
public Object getCellEditorValue() {
return textfield.getText();
}
/**
* Apply the Editor for a selected option.
*/
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
if(row < parameters.size()) {
Parameter<?> option = parameters.getNode(row).param;
if(option instanceof FileParameter) {
FileParameter fp = (FileParameter) option;
File f = null;
mode = FileParameter.FileType.INPUT_FILE.equals(fp.getFileType()) ? FileDialog.LOAD : FileDialog.SAVE;
if(fp.isDefined()) {
f = fp.getValue();
}
textfield.setText(f != null ? f.getPath() : "");
}
}
textfield.requestFocus();
return panel;
}
}
/**
* Editor for choosing classes.
*
* @author Erich Schubert
*/
private class ClassListEditor extends AbstractCellEditor implements TableCellEditor, ActionListener, KeyListener {
/**
* Serial version number
*/
private static final long serialVersionUID = 1L;
/**
* We need a panel to put our components on.
*/
final JPanel panel;
/**
* Text field to store the name
*/
final JTextField textfield = new JTextField();
/**
* The button to open the file selector
*/
final JButton button = new JButton("+");
/**
* The popup menu.
*/
final TreePopup popup;
/**
* Tree model
*/
private TreeModel model;
/**
* Parameter we are currently editing.
*/
private Parameter<?> option;
/**
* Constructor.
*/
public ClassListEditor() {
textfield.addKeyListener(this);
button.addActionListener(this);
model = new DefaultTreeModel(new DefaultMutableTreeNode());
popup = new TreePopup(model);
popup.getTree().setRootVisible(false);
popup.addActionListener(this);
Icon classIcon = StockIcon.getStockIcon(StockIcon.GO_NEXT);
Icon packageIcon = StockIcon.getStockIcon(StockIcon.PACKAGE);
TreePopup.Renderer renderer = (TreePopup.Renderer) popup.getTree().getCellRenderer();
renderer.setLeafIcon(classIcon);
renderer.setFolderIcon(packageIcon);
panel = new DispatchingPanel(textfield);
panel.setLayout(new BorderLayout());
panel.add(textfield, BorderLayout.CENTER);
panel.add(button, BorderLayout.EAST);
textfield.setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 3));
}
/**
* Callback to show the popup menu
*/
@Override
public void actionPerformed(ActionEvent e) {
if(e.getSource() == button) {
popup.show(panel);
return;
}
if(e.getSource() == popup) {
if(e.getActionCommand() == TreePopup.ACTION_CANCELED) {
popup.setVisible(false);
textfield.requestFocus();
return;
}
TreePath path = popup.getTree().getSelectionPath();
final Object comp = (path != null) ? path.getLastPathComponent() : null;
if(comp instanceof ClassNode) {
ClassNode sel = (path != null) ? (ClassNode) comp : null;
String newClass = (sel != null) ? sel.getClassName() : null;
if(newClass != null && newClass.length() > 0) {
if(option instanceof ClassListParameter) {
String val = textfield.getText();
if(val.equals(DynamicParameters.STRING_OPTIONAL) //
|| val.startsWith(DynamicParameters.STRING_USE_DEFAULT)) {
val = "";
}
val = (val.length() > 0) ? val + ClassListParameter.LIST_SEP + newClass : newClass;
textfield.setText(val);
}
else {
textfield.setText(newClass);
}
popup.setVisible(false);
fireEditingStopped();
textfield.requestFocus();
}
}
return;
}
LoggingUtil.warning("Unrecognized action event in ClassListEditor: " + e);
}
/**
* Delegate getCellEditorValue to the text field.
*/
@Override
public Object getCellEditorValue() {
return textfield.getText();
}
@Override
public void keyTyped(KeyEvent e) {
// Ignore
}
@Override
public void keyPressed(KeyEvent e) {
if((e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0) {
if(e.getKeyCode() == KeyEvent.VK_SPACE //
|| e.getKeyCode() == KeyEvent.VK_ENTER //
|| e.getKeyCode() == KeyEvent.VK_DOWN //
|| e.getKeyCode() == KeyEvent.VK_KP_DOWN) {
if(!popup.isVisible()) {
popup.show(ClassListEditor.this.panel);
e.consume();
}
}
}
}
@Override
public void keyReleased(KeyEvent e) {
// Ignore
}
/**
* Apply the Editor for a selected option.
*/
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
if(row < parameters.size()) {
this.option = parameters.getNode(row).param;
TreeNode root;
// We can do dropdown choices for class parameters
if(option instanceof ClassListParameter<?>) {
ClassListParameter<?> cp = (ClassListParameter<?>) option;
root = ClassTree.build(cp.getKnownImplementations(), cp.getRestrictionClass().getPackage().getName());
button.setText("+");
}
else if(option instanceof ClassParameter<?>) {
ClassParameter<?> cp = (ClassParameter<?>) option;
root = ClassTree.build(cp.getKnownImplementations(), cp.getRestrictionClass().getPackage().getName());
button.setText("v");
}
else {
root = new DefaultMutableTreeNode();
}
if(option.isDefined()) {
if(option.tookDefaultValue()) {
textfield.setText(DynamicParameters.STRING_USE_DEFAULT + option.getDefaultValueAsString());
}
else {
textfield.setText(option.getValueAsString());
}
}
else {
textfield.setText("");
}
popup.getTree().setModel(new DefaultTreeModel(root));
}
return panel;
}
}
/**
* This Editor will adjust to the type of the Option: Sometimes just a plain
* text editor, sometimes a ComboBox to offer known choices, and sometime a
* file selector dialog.
*
* TODO: class list parameters etc.
*
* @author Erich Schubert
*
*/
private class AdjustingEditor extends AbstractCellEditor implements TableCellEditor {
/**
* Serial version
*/
private static final long serialVersionUID = 1L;
/**
* The dropdown editor
*/
private final DropdownEditor dropdownEditor;
/**
* The plain text cell editor
*/
private final DefaultCellEditor plaintextEditor;
/**
* The class list editor
*/
private final ClassListEditor classListEditor;
/**
* The file selector editor
*/
private final FileNameEditor fileNameEditor;
/**
* We need to remember which editor we delegated to, so we know whom to ask
* for the value entered.
*/
private TableCellEditor activeEditor;
/**
* Constructor.
*/
public AdjustingEditor() {
final JComboBox<String> combobox = new JComboBox<>();
combobox.setEditable(true);
this.dropdownEditor = new DropdownEditor(combobox);
JTextField tf = new JTextField();
tf.setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 3));
this.plaintextEditor = new DefaultCellEditor(tf);
this.classListEditor = new ClassListEditor();
this.fileNameEditor = new FileNameEditor();
}
@Override
public Object getCellEditorValue() {
if(activeEditor == null) {
return null;
}
return activeEditor.getCellEditorValue();
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
if(value instanceof String) {
String s = (String) value;
if(s.startsWith(DynamicParameters.STRING_USE_DEFAULT)) {
value = s.substring(DynamicParameters.STRING_USE_DEFAULT.length());
}
}
if(row < parameters.size()) {
Parameter<?> option = parameters.getNode(row).param;
if(option instanceof Flag) {
activeEditor = dropdownEditor;
return dropdownEditor.getTableCellEditorComponent(table, value, isSelected, row, column);
}
if(option instanceof ClassListParameter<?>) {
activeEditor = classListEditor;
return classListEditor.getTableCellEditorComponent(table, value, isSelected, row, column);
}
if(option instanceof ClassParameter<?>) {
activeEditor = classListEditor;
return classListEditor.getTableCellEditorComponent(table, value, isSelected, row, column);
}
if(option instanceof FileParameter) {
activeEditor = fileNameEditor;
return fileNameEditor.getTableCellEditorComponent(table, value, isSelected, row, column);
}
if(option instanceof EnumParameter<?>) {
activeEditor = dropdownEditor;
return dropdownEditor.getTableCellEditorComponent(table, value, isSelected, row, column);
}
}
activeEditor = plaintextEditor;
return plaintextEditor.getTableCellEditorComponent(table, value, isSelected, row, column);
}
}
/**
* This is a panel that will dispatch keystrokes to a particular component.
*
* This makes the tabular GUI much more user friendly.
*
* @author Erich Schubert
*
* @apiviz.exclude
*/
private class DispatchingPanel extends JPanel {
/**
* Serial version.
*/
private static final long serialVersionUID = 1L;
/**
* Component to dispatch to.
*/
protected JComponent component;
/**
* Constructor.
*
* @param component Component to dispatch to.
*/
public DispatchingPanel(JComponent component) {
super();
this.component = component;
setRequestFocusEnabled(true);
}
@Override
public void addNotify() {
super.addNotify();
component.requestFocus();
}
@Override
protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
InputMap map = component.getInputMap(condition);
ActionMap am = component.getActionMap();
if(map != null && am != null && isEnabled()) {
Object binding = map.get(ks);
Action action = (binding == null) ? null : am.get(binding);
if(action != null) {
return SwingUtilities.notifyAction(action, ks, e, component, e.getModifiers());
}
}
return false;
}
};
}